examples.24_money_amount_decimal
Demonstrates how to parse money amounts like '€12.34' or 'USD 12.34'
into a (currency, Decimal) tuple using a transformation pipeline.
This example shows:
- How to normalize various input formats (
'€12.34','usd 12.34','GBP 7.50').- How to convert the string amount into a
Decimal.- How to detect invalid or unsupported currency formats gracefully.
Run directly:
python examples/24_money_amount_decimal.py
Expected output:
good: OK ('EUR', Decimal('12.34')) good2: OK ('GBP', Decimal('7.50')) bad : EXCEPTION Value must be one of
, got ('AUD', Decimal('1.23'))
1"""Demonstrates how to parse money amounts like `'€12.34'` or `'USD 12.34'` 2into a `(currency, Decimal)` tuple using a transformation pipeline. 3 4This example shows: 5 * How to normalize various input formats (`'€12.34'`, `'usd 12.34'`, `'GBP 7.50'`). 6 * How to convert the string amount into a `Decimal`. 7 * How to detect invalid or unsupported currency formats gracefully. 8 9Run directly: 10 11 python examples/24_money_amount_decimal.py 12 13Expected output: 14 15 good: OK ('EUR', Decimal('12.34')) 16 good2: OK ('GBP', Decimal('7.50')) 17 bad : EXCEPTION Value must be one of <enum 'Currency'>, got ('AUD', Decimal('1.23')) 18""" 19 20import sys 21import pathlib 22from decimal import Decimal, InvalidOperation 23from enum import Enum 24from typing import List 25 26# --------------------------------------------------------------------------- 27# Make repo root importable when running this file directly 28# --------------------------------------------------------------------------- 29sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) 30 31from constrained_values import Response, Status, TypeValidationStrategy 32from constrained_values.value import TransformationStrategy, ConstrainedValue, PipeLineStrategy 33 34 35class Currency(Enum): 36 """Supported currencies for this example.""" 37 USD = "USD" 38 EUR = "EUR" 39 GBP = "GBP" 40 41 42class Strip(TransformationStrategy[str, str]): 43 """Trim leading and trailing whitespace from a string.""" 44 45 def transform(self, value: str) -> Response[str]: 46 """Strip whitespace from the input string. 47 48 Args: 49 value: The raw input string. 50 51 Returns: 52 Response[str]: Contains: 53 * `status = Status.OK` 54 * `details = "strip"` 55 * `value` — the trimmed string 56 """ 57 return Response(Status.OK, "strip", value.strip()) 58 59 60class NormalizeCurrencyPrefix(TransformationStrategy[str, tuple]): 61 """Normalize currency prefixes and symbols. 62 63 Accepts strings like `'EUR 12.34'`, `'€12.34'`, or `'usd 12.34'` 64 and converts them into a tuple `(currency_code, amount_string)`. 65 66 If the input cannot be parsed, the strategy returns `Status.EXCEPTION`. 67 """ 68 69 SYMBOLS = {"€": "EUR", "$": "USD", "£": "GBP"} 70 71 def transform(self, value: str) -> Response[tuple]: 72 """Normalize currency and extract amount substring. 73 74 Args: 75 value: The string containing currency and amount. 76 77 Returns: 78 Response[tuple]: Contains `(currency_code, amount_str)` or 79 an error response if parsing fails. 80 """ 81 v = value.strip() 82 if not v: 83 return Response(Status.EXCEPTION, "empty", None) 84 if v[0] in self.SYMBOLS: 85 cur = self.SYMBOLS[v[0]] 86 amt = v[1:].strip() 87 return Response(Status.OK, "symbol", (cur, amt)) 88 parts = v.split(None, 1) 89 if len(parts) == 2: 90 cur, amt = parts[0].upper(), parts[1].strip() 91 return Response(Status.OK, "prefix", (cur, amt)) 92 return Response(Status.EXCEPTION, "format 'CUR 12.34' or '€12.34'", None) 93 94 95class ParseAmount(TransformationStrategy[tuple, tuple]): 96 """Convert a `(currency, amount_string)` pair into a `(currency, Decimal)` tuple.""" 97 98 def transform(self, value: tuple) -> Response[tuple]: 99 """Attempt to parse the amount as a Decimal. 100 101 Args: 102 value: Tuple `(currency_code, amount_str)`. 103 104 Returns: 105 Response[tuple]: Contains: 106 * `status = Status.OK` with `(currency_code, Decimal(amount))` 107 * `status = Status.EXCEPTION` if parsing fails 108 """ 109 cur, amt_s = value 110 try: 111 return Response(Status.OK, "decimal", (cur, Decimal(amt_s))) 112 except (InvalidOperation, ValueError) as e: 113 return Response(Status.EXCEPTION, f"bad decimal: {e}", None) 114 115 116from constrained_values import EnumValidationStrategy 117 118class MoneyConfig(ConstrainedValue[tuple]): 119 """A `ConstrainedValue` that parses a money amount into `(currency, Decimal)`. 120 121 The transformation pipeline: 122 1. `TypeValidationStrategy(str)` — ensures the input is a string. 123 2. `Strip()` — trims leading/trailing spaces. 124 3. `NormalizeCurrencyPrefix()` — extracts `(currency, amount_string)`. 125 4. `ParseAmount()` — converts amount to `Decimal`. 126 5. `EnumValidationStrategy(Currency)` — ensures currency is supported. 127 """ 128 129 def get_strategies(self) -> List[PipeLineStrategy]: 130 """Return the full transformation pipeline.""" 131 return [ 132 TypeValidationStrategy(str), 133 Strip(), 134 NormalizeCurrencyPrefix(), 135 ParseAmount(), 136 EnumValidationStrategy(Currency), 137 ] 138 139 140 141def main() -> None: 142 """Run the money parsing demonstration. 143 144 Creates several :class:`MoneyConfig` instances to parse different 145 currency string formats and prints their results. 146 147 Steps: 148 1. `"€12.34"` → OK, parsed as (`'EUR'`, Decimal('12.34')). 149 2. `"gbp 7.50"` → OK, parsed as (`'GBP'`, Decimal('7.50')). 150 3. `"AUD 1.23"` → EXCEPTION, unsupported currency format. 151 152 Prints: 153 * `"good: OK ('EUR', Decimal('12.34'))"` 154 * `"good2: OK ('GBP', Decimal('7.50'))"` 155 * `"bad : EXCEPTION Value must be one of <enum 'Currency'>, got ('AUD', Decimal('1.23'))"` 156 """ 157 good = MoneyConfig("€12.34") 158 print("good:", good.status.name, good.value) 159 good2 = MoneyConfig("gbp 7.50") 160 print("good2:", good2.status.name, good2.value) 161 bad = MoneyConfig("AUD 1.23") 162 print("bad :", bad.status.name, bad.details) 163 164 165if __name__ == "__main__": 166 main()
36class Currency(Enum): 37 """Supported currencies for this example.""" 38 USD = "USD" 39 EUR = "EUR" 40 GBP = "GBP"
Supported currencies for this example.
Inherited Members
- enum.Enum
- name
- value
43class Strip(TransformationStrategy[str, str]): 44 """Trim leading and trailing whitespace from a string.""" 45 46 def transform(self, value: str) -> Response[str]: 47 """Strip whitespace from the input string. 48 49 Args: 50 value: The raw input string. 51 52 Returns: 53 Response[str]: Contains: 54 * `status = Status.OK` 55 * `details = "strip"` 56 * `value` — the trimmed string 57 """ 58 return Response(Status.OK, "strip", value.strip())
Trim leading and trailing whitespace from a string.
46 def transform(self, value: str) -> Response[str]: 47 """Strip whitespace from the input string. 48 49 Args: 50 value: The raw input string. 51 52 Returns: 53 Response[str]: Contains: 54 * `status = Status.OK` 55 * `details = "strip"` 56 * `value` — the trimmed string 57 """ 58 return Response(Status.OK, "strip", value.strip())
Strip whitespace from the input string.
Arguments:
- value: The raw input string.
Returns:
Response[str]: Contains: *
status = Status.OK*details = "strip"*value— the trimmed string
61class NormalizeCurrencyPrefix(TransformationStrategy[str, tuple]): 62 """Normalize currency prefixes and symbols. 63 64 Accepts strings like `'EUR 12.34'`, `'€12.34'`, or `'usd 12.34'` 65 and converts them into a tuple `(currency_code, amount_string)`. 66 67 If the input cannot be parsed, the strategy returns `Status.EXCEPTION`. 68 """ 69 70 SYMBOLS = {"€": "EUR", "$": "USD", "£": "GBP"} 71 72 def transform(self, value: str) -> Response[tuple]: 73 """Normalize currency and extract amount substring. 74 75 Args: 76 value: The string containing currency and amount. 77 78 Returns: 79 Response[tuple]: Contains `(currency_code, amount_str)` or 80 an error response if parsing fails. 81 """ 82 v = value.strip() 83 if not v: 84 return Response(Status.EXCEPTION, "empty", None) 85 if v[0] in self.SYMBOLS: 86 cur = self.SYMBOLS[v[0]] 87 amt = v[1:].strip() 88 return Response(Status.OK, "symbol", (cur, amt)) 89 parts = v.split(None, 1) 90 if len(parts) == 2: 91 cur, amt = parts[0].upper(), parts[1].strip() 92 return Response(Status.OK, "prefix", (cur, amt)) 93 return Response(Status.EXCEPTION, "format 'CUR 12.34' or '€12.34'", None)
Normalize currency prefixes and symbols.
Accepts strings like 'EUR 12.34', '€12.34', or 'usd 12.34'
and converts them into a tuple (currency_code, amount_string).
If the input cannot be parsed, the strategy returns Status.EXCEPTION.
72 def transform(self, value: str) -> Response[tuple]: 73 """Normalize currency and extract amount substring. 74 75 Args: 76 value: The string containing currency and amount. 77 78 Returns: 79 Response[tuple]: Contains `(currency_code, amount_str)` or 80 an error response if parsing fails. 81 """ 82 v = value.strip() 83 if not v: 84 return Response(Status.EXCEPTION, "empty", None) 85 if v[0] in self.SYMBOLS: 86 cur = self.SYMBOLS[v[0]] 87 amt = v[1:].strip() 88 return Response(Status.OK, "symbol", (cur, amt)) 89 parts = v.split(None, 1) 90 if len(parts) == 2: 91 cur, amt = parts[0].upper(), parts[1].strip() 92 return Response(Status.OK, "prefix", (cur, amt)) 93 return Response(Status.EXCEPTION, "format 'CUR 12.34' or '€12.34'", None)
Normalize currency and extract amount substring.
Arguments:
- value: The string containing currency and amount.
Returns:
Response[tuple]: Contains
(currency_code, amount_str)or an error response if parsing fails.
96class ParseAmount(TransformationStrategy[tuple, tuple]): 97 """Convert a `(currency, amount_string)` pair into a `(currency, Decimal)` tuple.""" 98 99 def transform(self, value: tuple) -> Response[tuple]: 100 """Attempt to parse the amount as a Decimal. 101 102 Args: 103 value: Tuple `(currency_code, amount_str)`. 104 105 Returns: 106 Response[tuple]: Contains: 107 * `status = Status.OK` with `(currency_code, Decimal(amount))` 108 * `status = Status.EXCEPTION` if parsing fails 109 """ 110 cur, amt_s = value 111 try: 112 return Response(Status.OK, "decimal", (cur, Decimal(amt_s))) 113 except (InvalidOperation, ValueError) as e: 114 return Response(Status.EXCEPTION, f"bad decimal: {e}", None)
Convert a (currency, amount_string) pair into a (currency, Decimal) tuple.
99 def transform(self, value: tuple) -> Response[tuple]: 100 """Attempt to parse the amount as a Decimal. 101 102 Args: 103 value: Tuple `(currency_code, amount_str)`. 104 105 Returns: 106 Response[tuple]: Contains: 107 * `status = Status.OK` with `(currency_code, Decimal(amount))` 108 * `status = Status.EXCEPTION` if parsing fails 109 """ 110 cur, amt_s = value 111 try: 112 return Response(Status.OK, "decimal", (cur, Decimal(amt_s))) 113 except (InvalidOperation, ValueError) as e: 114 return Response(Status.EXCEPTION, f"bad decimal: {e}", None)
Attempt to parse the amount as a Decimal.
Arguments:
- value: Tuple
(currency_code, amount_str).
Returns:
Response[tuple]: Contains: *
status = Status.OKwith(currency_code, Decimal(amount))*status = Status.EXCEPTIONif parsing fails
119class MoneyConfig(ConstrainedValue[tuple]): 120 """A `ConstrainedValue` that parses a money amount into `(currency, Decimal)`. 121 122 The transformation pipeline: 123 1. `TypeValidationStrategy(str)` — ensures the input is a string. 124 2. `Strip()` — trims leading/trailing spaces. 125 3. `NormalizeCurrencyPrefix()` — extracts `(currency, amount_string)`. 126 4. `ParseAmount()` — converts amount to `Decimal`. 127 5. `EnumValidationStrategy(Currency)` — ensures currency is supported. 128 """ 129 130 def get_strategies(self) -> List[PipeLineStrategy]: 131 """Return the full transformation pipeline.""" 132 return [ 133 TypeValidationStrategy(str), 134 Strip(), 135 NormalizeCurrencyPrefix(), 136 ParseAmount(), 137 EnumValidationStrategy(Currency), 138 ]
A ConstrainedValue that parses a money amount into (currency, Decimal).
The transformation pipeline:
TypeValidationStrategy(str)— ensures the input is a string.Strip()— trims leading/trailing spaces.NormalizeCurrencyPrefix()— extracts(currency, amount_string).ParseAmount()— converts amount toDecimal.EnumValidationStrategy(Currency)— ensures currency is supported.
130 def get_strategies(self) -> List[PipeLineStrategy]: 131 """Return the full transformation pipeline.""" 132 return [ 133 TypeValidationStrategy(str), 134 Strip(), 135 NormalizeCurrencyPrefix(), 136 ParseAmount(), 137 EnumValidationStrategy(Currency), 138 ]
Return the full transformation pipeline.
Inherited Members
- constrained_values.value.ConstrainedValue
- ConstrainedValue
- status
- details
- value
- unwrap
- ok
142def main() -> None: 143 """Run the money parsing demonstration. 144 145 Creates several :class:`MoneyConfig` instances to parse different 146 currency string formats and prints their results. 147 148 Steps: 149 1. `"€12.34"` → OK, parsed as (`'EUR'`, Decimal('12.34')). 150 2. `"gbp 7.50"` → OK, parsed as (`'GBP'`, Decimal('7.50')). 151 3. `"AUD 1.23"` → EXCEPTION, unsupported currency format. 152 153 Prints: 154 * `"good: OK ('EUR', Decimal('12.34'))"` 155 * `"good2: OK ('GBP', Decimal('7.50'))"` 156 * `"bad : EXCEPTION Value must be one of <enum 'Currency'>, got ('AUD', Decimal('1.23'))"` 157 """ 158 good = MoneyConfig("€12.34") 159 print("good:", good.status.name, good.value) 160 good2 = MoneyConfig("gbp 7.50") 161 print("good2:", good2.status.name, good2.value) 162 bad = MoneyConfig("AUD 1.23") 163 print("bad :", bad.status.name, bad.details)
Run the money parsing demonstration.
Creates several MoneyConfig instances to parse different
currency string formats and prints their results.
Steps:
"€12.34"→ OK, parsed as ('EUR', Decimal('12.34'))."gbp 7.50"→ OK, parsed as ('GBP', Decimal('7.50'))."AUD 1.23"→ EXCEPTION, unsupported currency format.
Prints:
"good: OK ('EUR', Decimal('12.34'))""good2: OK ('GBP', Decimal('7.50'))""bad : EXCEPTION Value must be one of <enum 'Currency'>, got ('AUD', Decimal('1.23'))"