1818import pytest
1919
2020import freshdata as fd
21- from freshdata .semantic .experts import parse_currency , parse_currency_parts
21+ from freshdata .config import CleanConfig
22+ from freshdata .engine .context import infer_role
23+ from freshdata .semantic .experts import (
24+ CurrencyStringExpert ,
25+ parse_currency ,
26+ parse_currency_parts ,
27+ )
28+ from freshdata .semantic .types import SemanticColumnInfo
2229
2330FIXTURES_DIR = Path (__file__ ).parent / "fixtures"
2431
@@ -121,6 +128,7 @@ def test_clean_does_not_scale_european_amounts(text, expected):
121128
122129
123130def test_clean_financial_ledger_fixture_respects_locale_and_accounting_values ():
131+ """Verify financial ledger cleaning preserves row counts, dtypes, and values."""
124132 fixture = pd .read_csv (FIXTURES_DIR / "financial_ledger.csv" )
125133 expectations = json .loads (
126134 (FIXTURES_DIR / "financial_ledger.expectations.json" ).read_text ()
@@ -137,3 +145,102 @@ def test_clean_financial_ledger_fixture_respects_locale_and_accounting_values():
137145 actual = cleaned .loc [cleaned ["transaction_id" ] == transaction_id , "amount" ]
138146 assert len (actual ) == 1
139147 assert actual .iloc [0 ] == expected
148+
149+
150+ # -- PR #503 review fixes verification --------------------------------------
151+
152+
153+ @pytest .mark .parametrize (
154+ ("text" , "expected" ),
155+ [
156+ ("(-$1,250.00)" , - 1250.0 ),
157+ ("($-1,250.00)" , - 1250.0 ),
158+ ("(-EUR 500.00)" , - 500.0 ),
159+ ("(-10.50)" , - 10.50 ),
160+ ],
161+ )
162+ def test_accounting_negatives_preserve_existing_negative_sign (text , expected ):
163+ """An explicit minus sign inside accounting parentheses must not flip positive."""
164+ val , _ = parse_currency_parts (text )
165+ assert val == expected
166+
167+
168+ @pytest .mark .parametrize (
169+ "text" ,
170+ [
171+ "(10 kg)" ,
172+ "(page 3)" ,
173+ "(see item 42)" ,
174+ "(10)" ,
175+ "(100)" ,
176+ "(10%)" ,
177+ "(10 / 20)" ,
178+ "( - )" ,
179+ ],
180+ )
181+ def test_unit_and_word_strings_in_parentheses_are_not_currency (text ):
182+ """Parenthesized units and citations must not be treated as negative currency."""
183+ val , ambiguous = parse_currency_parts (text )
184+ assert val is None
185+ assert ambiguous is False
186+
187+
188+ def test_unmarked_parenthetical_with_ambiguous_separators_flagged ():
189+ """Unmarked numbers like (1,250) must be flagged ambiguous and routed to review."""
190+ val , ambiguous = parse_currency_parts ("(1,250)" )
191+ assert val == - 1250.0
192+ assert ambiguous is True
193+
194+ val2 , ambiguous2 = parse_currency_parts ("(1.250)" )
195+ assert val2 == - 1.25
196+ assert ambiguous2 is True
197+
198+ expert = CurrencyStringExpert ()
199+ info = SemanticColumnInfo (
200+ name = "amount" ,
201+ role = "numeric" ,
202+ n_nonnull = 1 ,
203+ nunique = 1 ,
204+ high_cardinality = False ,
205+ preserve = False ,
206+ free_text = False ,
207+ numeric_like = True ,
208+ boolean_like = False ,
209+ money_like = True ,
210+ unit_like = False ,
211+ identifier_like = False ,
212+ )
213+ series = pd .Series (["(1,250)" ])
214+ proposals = expert .propose (series , info )
215+ assert len (proposals ) == 1
216+ assert proposals [0 ].risk == "high"
217+ assert proposals [0 ].confidence <= 0.60
218+
219+
220+ def test_payment_id_retains_identifier_role ():
221+ """Names matching _ID_NAME must retain id role even when matching _MONEY_NAME."""
222+ cfg = CleanConfig ()
223+ # Repeating values (nunique != non_null) ensure role is not inferred purely by cardinality
224+ for name in ("payment_id" , "charge_id" , "fee_id" , "payment_key" , "balance_uuid" ):
225+ series = pd .Series (["ID1" , "ID1" , "ID2" ])
226+ assert infer_role (name , series , cfg ) == "id"
227+
228+
229+ def test_free_text_monetary_columns_stay_protected ():
230+ """Free-text columns marked money_like must stay protected from currency conversion."""
231+ expert = CurrencyStringExpert ()
232+ info = SemanticColumnInfo (
233+ name = "notes" ,
234+ role = "text" ,
235+ n_nonnull = 3 ,
236+ nunique = 3 ,
237+ high_cardinality = False ,
238+ preserve = False ,
239+ free_text = True ,
240+ numeric_like = False ,
241+ boolean_like = False ,
242+ money_like = True ,
243+ unit_like = False ,
244+ identifier_like = False ,
245+ )
246+ assert expert .applies (info ) is False
0 commit comments