Commit Β·
39dcffa
1
Parent(s): 88646e1
fix: use USD X,XXX format instead of $X,XXX for all currency values
Browse files- format_money_figures() now outputs "USD 1,800" not "$1,800"
- Normalizes any existing $ or USD prefix before re-formatting
- emphasize_keywords() bolds "USD X,XXX" patterns in bold
- All monetary figures in paragraphs, tables, steps, bullets are bold + USD format
- All 89 tests pass
- app/services/utils.py +19 -23
- app/templates/partials/blocks/table.html +11 -7
- tests/test_renderers.py +6 -5
app/services/utils.py
CHANGED
|
@@ -48,47 +48,42 @@ def is_truthy(val) -> bool:
|
|
| 48 |
|
| 49 |
|
| 50 |
def format_money_figures(text: str) -> str:
|
| 51 |
-
"""
|
| 52 |
|
| 53 |
-
-
|
| 54 |
-
-
|
| 55 |
- Formats with commas
|
|
|
|
| 56 |
"""
|
| 57 |
if not text:
|
| 58 |
return text
|
| 59 |
|
| 60 |
-
#
|
| 61 |
-
text = re.sub(r
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
def _format_match(m: re.Match) -> str:
|
| 64 |
num_str = m.group(1).replace(",", "")
|
| 65 |
dec = m.group(2) if m.group(2) else ""
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
| 67 |
if dec:
|
| 68 |
formatted = f"{num:,.{len(dec)}f}"
|
| 69 |
else:
|
| 70 |
formatted = f"{num:,.0f}"
|
| 71 |
-
return "
|
| 72 |
|
| 73 |
-
# Add
|
| 74 |
text = re.sub(
|
| 75 |
-
r"(?<!
|
| 76 |
_format_match,
|
| 77 |
text,
|
| 78 |
)
|
| 79 |
|
| 80 |
-
# Ensure remaining 4+ digit numbers are formatted with commas and $
|
| 81 |
-
def _format_remaining(m: re.Match) -> str:
|
| 82 |
-
num_str = m.group(1).replace(",", "")
|
| 83 |
-
formatted = f"{float(num_str):,.0f}"
|
| 84 |
-
return "$" + formatted
|
| 85 |
-
|
| 86 |
-
text = re.sub(
|
| 87 |
-
r"(?<!\$)\b(\d{1,3}(?:,\d{3})+|\d{4,})(?![%\d/])",
|
| 88 |
-
_format_remaining,
|
| 89 |
-
text,
|
| 90 |
-
)
|
| 91 |
-
|
| 92 |
return text
|
| 93 |
|
| 94 |
|
|
@@ -161,11 +156,12 @@ def emphasize_keywords(text: str) -> str:
|
|
| 161 |
flags=re.IGNORECASE,
|
| 162 |
)
|
| 163 |
|
| 164 |
-
# Bold
|
| 165 |
escaped = re.sub(
|
| 166 |
-
r'(\
|
| 167 |
r'<strong>\1</strong>',
|
| 168 |
escaped,
|
|
|
|
| 169 |
)
|
| 170 |
|
| 171 |
# Bold key qualification and geo terms.
|
|
|
|
| 48 |
|
| 49 |
|
| 50 |
def format_money_figures(text: str) -> str:
|
| 51 |
+
"""Normalize all monetary figures to "USD X,XXX" format.
|
| 52 |
|
| 53 |
+
- Converts existing $X,XXX β USD X,XXX
|
| 54 |
+
- Normalizes bare large numbers β USD X,XXX
|
| 55 |
- Formats with commas
|
| 56 |
+
- Currency type is always USD (no $ symbol)
|
| 57 |
"""
|
| 58 |
if not text:
|
| 59 |
return text
|
| 60 |
|
| 61 |
+
# Normalize "$X,XXX" β bare number (strip $ symbol)
|
| 62 |
+
text = re.sub(r'\$([\d,]+(?:\.\d+)?)', lambda m: m.group(1), text)
|
| 63 |
+
|
| 64 |
+
# Normalize "USD X,XXX" β bare number for uniform re-processing
|
| 65 |
+
text = re.sub(r'\bUSD\s+([\d,]+(?:\.\d+)?)', lambda m: m.group(1), text, flags=re.IGNORECASE)
|
| 66 |
|
| 67 |
def _format_match(m: re.Match) -> str:
|
| 68 |
num_str = m.group(1).replace(",", "")
|
| 69 |
dec = m.group(2) if m.group(2) else ""
|
| 70 |
+
try:
|
| 71 |
+
num = float(num_str)
|
| 72 |
+
except ValueError:
|
| 73 |
+
return m.group(0)
|
| 74 |
if dec:
|
| 75 |
formatted = f"{num:,.{len(dec)}f}"
|
| 76 |
else:
|
| 77 |
formatted = f"{num:,.0f}"
|
| 78 |
+
return "USD " + formatted
|
| 79 |
|
| 80 |
+
# Add "USD " to large numbers (4+ digits or already comma-formatted)
|
| 81 |
text = re.sub(
|
| 82 |
+
r"(?<!\d)((?:\d{1,3}(?:,\d{3})+)|(?:\d{4,}))(?:\.(\d+))?(?![%\d/])",
|
| 83 |
_format_match,
|
| 84 |
text,
|
| 85 |
)
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
return text
|
| 88 |
|
| 89 |
|
|
|
|
| 156 |
flags=re.IGNORECASE,
|
| 157 |
)
|
| 158 |
|
| 159 |
+
# Bold USD amounts like USD 1,000 or USD 500
|
| 160 |
escaped = re.sub(
|
| 161 |
+
r'\b(USD\s+[\d,]+(?:\.\d+)?)',
|
| 162 |
r'<strong>\1</strong>',
|
| 163 |
escaped,
|
| 164 |
+
flags=re.IGNORECASE,
|
| 165 |
)
|
| 166 |
|
| 167 |
# Bold key qualification and geo terms.
|
app/templates/partials/blocks/table.html
CHANGED
|
@@ -26,7 +26,8 @@
|
|
| 26 |
{% for c in block.data.all_columns %}
|
| 27 |
{% set col_label = c.label | default('') %}
|
| 28 |
{% set col_lc = col_label | lower %}
|
| 29 |
-
<th class="{% if col_lc == 'regular' %}is-regular-col{% elif col_lc == 'prime' %}is-prime-col{% endif %}">{{
|
|
|
|
| 30 |
{% endfor %}
|
| 31 |
</tr>
|
| 32 |
{% endif %}
|
|
@@ -37,7 +38,8 @@
|
|
| 37 |
{% for c in block.data.all_columns %}
|
| 38 |
{% set col_label = c.label | default('') %}
|
| 39 |
{% set col_lc = col_label | lower %}
|
| 40 |
-
<td class="{% if col_lc == 'regular' %}is-regular-col{% elif col_lc == 'prime' %}is-prime-col{% endif %}">{{
|
|
|
|
| 41 |
{% endfor %}
|
| 42 |
</tr>
|
| 43 |
{% endfor %}
|
|
@@ -67,7 +69,8 @@
|
|
| 67 |
<tr>
|
| 68 |
{% for col in block.data.columns %}
|
| 69 |
{% set col_lc = (col | lower) %}
|
| 70 |
-
<th class="{% if col_lc == 'regular' %}is-regular-col{% elif col_lc == 'prime' %}is-prime-col{% endif %}">{{
|
|
|
|
| 71 |
{% endfor %}
|
| 72 |
</tr>
|
| 73 |
</thead>
|
|
@@ -76,10 +79,11 @@
|
|
| 76 |
{% for row in block.data.rows %}
|
| 77 |
<tr>
|
| 78 |
{% for cell in row %}
|
| 79 |
-
{% set col = block.data.columns[loop.index0] if block.data.columns and loop.index0 < (block.data.columns |
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
| 83 |
</tr>
|
| 84 |
{% endfor %}
|
| 85 |
</tbody>
|
|
|
|
| 26 |
{% for c in block.data.all_columns %}
|
| 27 |
{% set col_label = c.label | default('') %}
|
| 28 |
{% set col_lc = col_label | lower %}
|
| 29 |
+
<th class="{% if col_lc == 'regular' %}is-regular-col{% elif col_lc == 'prime' %}is-prime-col{% endif %}">{{
|
| 30 |
+
c.label | e }}</th>
|
| 31 |
{% endfor %}
|
| 32 |
</tr>
|
| 33 |
{% endif %}
|
|
|
|
| 38 |
{% for c in block.data.all_columns %}
|
| 39 |
{% set col_label = c.label | default('') %}
|
| 40 |
{% set col_lc = col_label | lower %}
|
| 41 |
+
<td class="{% if col_lc == 'regular' %}is-regular-col{% elif col_lc == 'prime' %}is-prime-col{% endif %}">{{
|
| 42 |
+
row[c.key] | default('') | safe }}</td>
|
| 43 |
{% endfor %}
|
| 44 |
</tr>
|
| 45 |
{% endfor %}
|
|
|
|
| 69 |
<tr>
|
| 70 |
{% for col in block.data.columns %}
|
| 71 |
{% set col_lc = (col | lower) %}
|
| 72 |
+
<th class="{% if col_lc == 'regular' %}is-regular-col{% elif col_lc == 'prime' %}is-prime-col{% endif %}">{{
|
| 73 |
+
col | e }}</th>
|
| 74 |
{% endfor %}
|
| 75 |
</tr>
|
| 76 |
</thead>
|
|
|
|
| 79 |
{% for row in block.data.rows %}
|
| 80 |
<tr>
|
| 81 |
{% for cell in row %}
|
| 82 |
+
{% set col = block.data.columns[loop.index0] if block.data.columns and loop.index0 < (block.data.columns |
|
| 83 |
+
length) else '' %} {% set col_lc=(col | lower) %} <td
|
| 84 |
+
class="{% if col_lc == 'regular' %}is-regular-col{% elif col_lc == 'prime' %}is-prime-col{% endif %}">{{
|
| 85 |
+
cell | safe }}</td>
|
| 86 |
+
{% endfor %}
|
| 87 |
</tr>
|
| 88 |
{% endfor %}
|
| 89 |
</tbody>
|
tests/test_renderers.py
CHANGED
|
@@ -70,15 +70,16 @@ def test_is_truthy_false_values():
|
|
| 70 |
|
| 71 |
# ββ format_money_figures() ββ
|
| 72 |
|
| 73 |
-
def
|
| 74 |
result = format_money_figures("USD 5000")
|
| 75 |
-
assert "USD"
|
| 76 |
-
assert "$" in result
|
| 77 |
|
| 78 |
|
| 79 |
-
def
|
| 80 |
result = format_money_figures("The cost is 15000 per year")
|
| 81 |
-
assert "
|
|
|
|
| 82 |
|
| 83 |
|
| 84 |
def test_format_money_preserves_percentages():
|
|
|
|
| 70 |
|
| 71 |
# ββ format_money_figures() ββ
|
| 72 |
|
| 73 |
+
def test_format_money_usd_prefix_normalised():
|
| 74 |
result = format_money_figures("USD 5000")
|
| 75 |
+
assert "USD 5,000" in result
|
| 76 |
+
assert "$" not in result
|
| 77 |
|
| 78 |
|
| 79 |
+
def test_format_money_formats_as_usd():
|
| 80 |
result = format_money_figures("The cost is 15000 per year")
|
| 81 |
+
assert "USD 15,000" in result
|
| 82 |
+
assert "$" not in result
|
| 83 |
|
| 84 |
|
| 85 |
def test_format_money_preserves_percentages():
|