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()
class Currency(enum.Enum):
36class Currency(Enum):
37    """Supported currencies for this example."""
38    USD = "USD"
39    EUR = "EUR"
40    GBP = "GBP"

Supported currencies for this example.

USD = <Currency.USD: 'USD'>
EUR = <Currency.EUR: 'EUR'>
GBP = <Currency.GBP: 'GBP'>
Inherited Members
enum.Enum
name
value
class Strip(constrained_values.value.TransformationStrategy[str, str]):
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.

def transform(self, value: str) -> constrained_values.Response[str]:
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

class NormalizeCurrencyPrefix(constrained_values.value.TransformationStrategy[str, tuple]):
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.

SYMBOLS = {'€': 'EUR', '$': 'USD', '£': 'GBP'}
def transform(self, value: str) -> constrained_values.Response[tuple]:
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.

class ParseAmount(constrained_values.value.TransformationStrategy[tuple, tuple]):
 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.

def transform(self, value: tuple) -> constrained_values.Response[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.OK with (currency_code, Decimal(amount)) * status = Status.EXCEPTION if parsing fails

class MoneyConfig(constrained_values.value.ConstrainedValue[tuple]):
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:
  1. TypeValidationStrategy(str) — ensures the input is a string.
  2. Strip() — trims leading/trailing spaces.
  3. NormalizeCurrencyPrefix() — extracts (currency, amount_string).
  4. ParseAmount() — converts amount to Decimal.
  5. EnumValidationStrategy(Currency) — ensures currency is supported.
def get_strategies(self) -> List[constrained_values.value.PipeLineStrategy]:
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
def main() -> None:
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:
  1. "€12.34" → OK, parsed as ('EUR', Decimal('12.34')).
  2. "gbp 7.50" → OK, parsed as ('GBP', Decimal('7.50')).
  3. "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'))"