pl2html 0.4.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pl2html-0.4.0 → pl2html-0.5.0}/PKG-INFO +5 -5
- {pl2html-0.4.0 → pl2html-0.5.0}/README.md +4 -4
- pl2html-0.5.0/pl2html/__init__.py +1 -0
- pl2html-0.4.0/pl2html/__init__.py → pl2html-0.5.0/pl2html/compiler.py +85 -12
- {pl2html-0.4.0 → pl2html-0.5.0}/pl2html/formats.py +111 -80
- {pl2html-0.4.0 → pl2html-0.5.0}/pyproject.toml +1 -1
- {pl2html-0.4.0 → pl2html-0.5.0}/pl2html/styles.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pl2html
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Dist: polars>=1.42.1
|
|
6
6
|
Requires-Python: >=3.14
|
|
@@ -40,7 +40,7 @@ By default, `pl2html` infers datatypes to securely render human-readable tables.
|
|
|
40
40
|
|
|
41
41
|
```python
|
|
42
42
|
import polars as pl
|
|
43
|
-
from pl2html import to_html
|
|
43
|
+
from pl2html.compiler import to_html
|
|
44
44
|
|
|
45
45
|
df = pl.DataFrame(
|
|
46
46
|
{
|
|
@@ -78,7 +78,7 @@ Output:
|
|
|
78
78
|
|
|
79
79
|
```python
|
|
80
80
|
import polars as pl
|
|
81
|
-
from pl2html import to_html
|
|
81
|
+
from pl2html.compiler import to_html
|
|
82
82
|
from pl2html import formats as fmt
|
|
83
83
|
|
|
84
84
|
df = pl.DataFrame(
|
|
@@ -107,7 +107,7 @@ Instead of embedding hacky structural HTML tags in your data, use the `attrs` pa
|
|
|
107
107
|
|
|
108
108
|
```python
|
|
109
109
|
import polars as pl
|
|
110
|
-
from pl2html import to_html
|
|
110
|
+
from pl2html.compiler import to_html
|
|
111
111
|
from pl2html.styles import data_color, rank_color
|
|
112
112
|
|
|
113
113
|
df = pl.DataFrame(
|
|
@@ -167,7 +167,7 @@ Output (github may override table colors, but they are there):
|
|
|
167
167
|
|
|
168
168
|
The library is explicitly divided into three decoupled layers:
|
|
169
169
|
|
|
170
|
-
### 1. Core Compiler (`
|
|
170
|
+
### 1. Core Compiler (`compiler.py`)
|
|
171
171
|
Responsible for reading the dataframe schema, iterating horizontally over visible columns, and joining tokens into structural row arrays natively in Rust.
|
|
172
172
|
* Exposed via `to_html(df, *, attrs=None, exclude_columns=None)`.
|
|
173
173
|
* Automatically assigns high-performance format fallbacks:
|
|
@@ -31,7 +31,7 @@ By default, `pl2html` infers datatypes to securely render human-readable tables.
|
|
|
31
31
|
|
|
32
32
|
```python
|
|
33
33
|
import polars as pl
|
|
34
|
-
from pl2html import to_html
|
|
34
|
+
from pl2html.compiler import to_html
|
|
35
35
|
|
|
36
36
|
df = pl.DataFrame(
|
|
37
37
|
{
|
|
@@ -69,7 +69,7 @@ Output:
|
|
|
69
69
|
|
|
70
70
|
```python
|
|
71
71
|
import polars as pl
|
|
72
|
-
from pl2html import to_html
|
|
72
|
+
from pl2html.compiler import to_html
|
|
73
73
|
from pl2html import formats as fmt
|
|
74
74
|
|
|
75
75
|
df = pl.DataFrame(
|
|
@@ -98,7 +98,7 @@ Instead of embedding hacky structural HTML tags in your data, use the `attrs` pa
|
|
|
98
98
|
|
|
99
99
|
```python
|
|
100
100
|
import polars as pl
|
|
101
|
-
from pl2html import to_html
|
|
101
|
+
from pl2html.compiler import to_html
|
|
102
102
|
from pl2html.styles import data_color, rank_color
|
|
103
103
|
|
|
104
104
|
df = pl.DataFrame(
|
|
@@ -158,7 +158,7 @@ Output (github may override table colors, but they are there):
|
|
|
158
158
|
|
|
159
159
|
The library is explicitly divided into three decoupled layers:
|
|
160
160
|
|
|
161
|
-
### 1. Core Compiler (`
|
|
161
|
+
### 1. Core Compiler (`compiler.py`)
|
|
162
162
|
Responsible for reading the dataframe schema, iterating horizontally over visible columns, and joining tokens into structural row arrays natively in Rust.
|
|
163
163
|
* Exposed via `to_html(df, *, attrs=None, exclude_columns=None)`.
|
|
164
164
|
* Automatically assigns high-performance format fallbacks:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.5.0'
|
|
@@ -10,8 +10,6 @@ from polars import (
|
|
|
10
10
|
when as _when,
|
|
11
11
|
)
|
|
12
12
|
|
|
13
|
-
__version__ = '0.4.0'
|
|
14
|
-
|
|
15
13
|
|
|
16
14
|
def _escape_polars_string(col_name: str) -> _Expr:
|
|
17
15
|
"""
|
|
@@ -126,10 +124,53 @@ def to_html(
|
|
|
126
124
|
*,
|
|
127
125
|
attrs: dict[str, dict[str, _Expr]] | None = None,
|
|
128
126
|
exclude_columns: list[str] | None = None,
|
|
127
|
+
formatters: _Expr | list[_Expr] | None = None,
|
|
129
128
|
) -> str:
|
|
130
|
-
"""
|
|
131
|
-
|
|
132
|
-
Accepts structural custom
|
|
129
|
+
"""Compiles a Polars DataFrame safely into an HTML string layout.
|
|
130
|
+
|
|
131
|
+
Accepts structural custom attribute mappings to handle layout modifications
|
|
132
|
+
and cell styling natively.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
df: The source Polars DataFrame or LazyFrame containing the data.
|
|
136
|
+
attrs: A dictionary mapping column names to cell attributes (e.g., style,
|
|
137
|
+
class). The inner dictionary values can be raw Polars expressions that
|
|
138
|
+
evaluate dynamically based on the column values.
|
|
139
|
+
exclude_columns: A list of column names to omit from the rendered HTML table.
|
|
140
|
+
formatters: A single Polars expression or a list of expressions used to
|
|
141
|
+
format column values into display strings (e.g., using `fmt_number`).
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
A raw HTML string representation of the styled and formatted table.
|
|
145
|
+
|
|
146
|
+
Note on Formatters Execution Order:
|
|
147
|
+
Usually, formatting expressions can be applied directly to the DataFrame
|
|
148
|
+
via `.with_columns()` before calling `to_html`. However, if your `attrs`
|
|
149
|
+
contain expressions that require numerical operations on data values (such
|
|
150
|
+
as computing percentage ranks or maximum thresholds via `rank_color`),
|
|
151
|
+
pre-formatting will convert those columns into `String` types too early and
|
|
152
|
+
cause downstream calculation panics (e.g., string division errors).
|
|
153
|
+
|
|
154
|
+
By passing your formatting expressions to the `formatters` parameter
|
|
155
|
+
instead, `to_html` guarantees they are scheduled to execute *after* the
|
|
156
|
+
numerical style attributes have been safely resolved against the raw data.
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
>>> import polars as pl
|
|
160
|
+
>>> from pl2html import to_html
|
|
161
|
+
>>> from pl2html.formats import fmt_number
|
|
162
|
+
>>>
|
|
163
|
+
>>> df = pl.DataFrame({"sprd": [10.0, 50.0, 100.0]})
|
|
164
|
+
>>>
|
|
165
|
+
>>> # A style rule that requires the column to remain numeric (Float64)
|
|
166
|
+
>>> attrs = {"sprd": {"style": pl.col("sprd") / pl.col("sprd").max()}}
|
|
167
|
+
>>>
|
|
168
|
+
>>> # Safe execution: styles are computed first, then text formatting is applied
|
|
169
|
+
>>> html = to_html(
|
|
170
|
+
... df,
|
|
171
|
+
... attrs=attrs,
|
|
172
|
+
... formatters=fmt_number(columns=["sprd"], decimals=2)
|
|
173
|
+
... )
|
|
133
174
|
"""
|
|
134
175
|
lf = df.lazy() if isinstance(df, _DataFrame) else df
|
|
135
176
|
|
|
@@ -139,19 +180,51 @@ def to_html(
|
|
|
139
180
|
schema = lf.collect_schema()
|
|
140
181
|
visible_columns = [c for c in schema.names() if c not in exclude_columns]
|
|
141
182
|
|
|
142
|
-
|
|
183
|
+
# === STEP 1: RESOLVE MATH/STYLE EXPRESSIONS ON NUMERIC DATA FIRST ===
|
|
184
|
+
# If there are style expressions, evaluate them into static string literals
|
|
185
|
+
# before applying the text formatters.
|
|
186
|
+
style_selects = []
|
|
187
|
+
style_maps = {}
|
|
188
|
+
|
|
189
|
+
for col_name, styles in attrs.items():
|
|
190
|
+
if col_name in visible_columns and 'style' in styles:
|
|
191
|
+
expr_key = f'__style_{col_name}'
|
|
192
|
+
style_selects.append(styles['style'].alias(expr_key))
|
|
193
|
+
style_maps[col_name] = expr_key
|
|
194
|
+
|
|
195
|
+
if style_selects:
|
|
196
|
+
# Collect just the evaluated style values using the numeric dataframe state
|
|
197
|
+
df = lf.collect()
|
|
198
|
+
lf = df.lazy()
|
|
199
|
+
resolved_styles_df = df.select(style_selects)
|
|
200
|
+
|
|
201
|
+
# Replace the lazy expressions in attrs with concrete string series expressions
|
|
202
|
+
new_attrs = {}
|
|
203
|
+
for col_name, styles in attrs.items():
|
|
204
|
+
new_attrs[col_name] = styles.copy()
|
|
205
|
+
if col_name in style_maps:
|
|
206
|
+
# Inject the pre-calculated style vector back as a harmless literal expression
|
|
207
|
+
new_attrs[col_name]['style'] = _lit(
|
|
208
|
+
resolved_styles_df.get_column(style_maps[col_name])
|
|
209
|
+
)
|
|
210
|
+
attrs = new_attrs
|
|
143
211
|
|
|
144
|
-
# 2
|
|
212
|
+
# === STEP 2: APPLY FORMATTERS TO CONVERT COLUMNS TO STRINGS ===
|
|
213
|
+
if formatters is not None:
|
|
214
|
+
if isinstance(formatters, _Expr):
|
|
215
|
+
formatters = [formatters]
|
|
216
|
+
lf = lf.with_columns(formatters)
|
|
217
|
+
|
|
218
|
+
# === STEP 3: BUILD HTML CELLS (Now 100% safe from string division errors!) ===
|
|
219
|
+
cell_expressions = []
|
|
145
220
|
for c in visible_columns:
|
|
146
|
-
cell_expressions.append(
|
|
221
|
+
cell_expressions.append(
|
|
222
|
+
_build_cell_expr(c, lf.collect_schema()[c], attrs)
|
|
223
|
+
)
|
|
147
224
|
|
|
148
|
-
# 3. Concatenate columns horizontally into rows
|
|
149
225
|
row_expr = _lit(' <tr>') + _concat_str(cell_expressions) + _lit('</tr>')
|
|
150
|
-
|
|
151
|
-
# 4. Generate wrappers and frame the query graph
|
|
152
226
|
html_header, html_footer = _build_html_skeleton(visible_columns)
|
|
153
227
|
|
|
154
|
-
# 5. Execute the query graph and pull out the raw python string directly
|
|
155
228
|
return (
|
|
156
229
|
lf.select(row_expr.alias('html_row'))
|
|
157
230
|
.select(
|
|
@@ -30,111 +30,98 @@ def _multicolumn(func):
|
|
|
30
30
|
_Columns = str | list[str]
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
compact: bool = False,
|
|
40
|
-
compact_system: _Literal['financial', 'engineering'] = 'financial',
|
|
41
|
-
use_seps: bool = True,
|
|
42
|
-
accounting: bool = False,
|
|
43
|
-
pattern: str = '{x}',
|
|
44
|
-
n_sigfig: int | None = None,
|
|
45
|
-
force_sign: bool = False,
|
|
46
|
-
) -> _Expr:
|
|
47
|
-
"""
|
|
48
|
-
Highly optimized, native Polars numeric formatter matching great_tables features.
|
|
49
|
-
Runs entirely in the parallel Rust engine without dropping into Python row loops.
|
|
50
|
-
"""
|
|
51
|
-
val = _col(columns)
|
|
52
|
-
if scale_by != 1.0:
|
|
53
|
-
val = val * scale_by
|
|
33
|
+
def _apply_compact_scaling(
|
|
34
|
+
val: _Expr, compact: bool, compact_system: str
|
|
35
|
+
) -> tuple[_Expr, _Expr]:
|
|
36
|
+
"""Handles logic block 1: Extracts compact suffix rules and scales values down."""
|
|
37
|
+
if not compact:
|
|
38
|
+
return val, _lit('')
|
|
54
39
|
|
|
55
40
|
abs_val = val.abs()
|
|
41
|
+
log10_expr = _when(abs_val > 0).then(abs_val.log10()).otherwise(_lit(0.0))
|
|
42
|
+
thousands_exponent = (log10_expr / 3.0).floor().cast(_Int32) * 3
|
|
56
43
|
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
44
|
+
# Force exponent to 0 for fractional numbers (< 1.0)
|
|
45
|
+
thousands_exponent = (
|
|
46
|
+
_when(thousands_exponent < 0)
|
|
47
|
+
.then(_lit(0))
|
|
48
|
+
.otherwise(thousands_exponent)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
system_chars = (
|
|
52
|
+
('k', 'M', 'G', 'T')
|
|
53
|
+
if compact_system == 'engineering'
|
|
54
|
+
else ('K', 'M', 'B', 'T')
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
suffix_chain = (
|
|
58
|
+
_when(thousands_exponent == 3)
|
|
59
|
+
.then(_lit(system_chars[0]))
|
|
60
|
+
.when(thousands_exponent == 6)
|
|
61
|
+
.then(_lit(system_chars[1]))
|
|
62
|
+
.when(thousands_exponent == 9)
|
|
63
|
+
.then(_lit(system_chars[2]))
|
|
64
|
+
.when(thousands_exponent == 12)
|
|
65
|
+
.then(_lit(system_chars[3]))
|
|
66
|
+
.otherwise(_lit(''))
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
divisor = _lit(10.0).pow(thousands_exponent.cast(_Float64))
|
|
70
|
+
return val / divisor, suffix_chain
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _compute_dynamic_precision(
|
|
74
|
+
val_scaled: _Expr, decimals: int, n_sigfig: int | None
|
|
75
|
+
) -> tuple[_Expr, _Expr | int]:
|
|
76
|
+
"""Handles logic block 2: Computes precision bounds safely avoiding non-finite floats, preserving nulls."""
|
|
77
|
+
|
|
78
|
+
# FIX: Explicitly target only NaN and Infinity, leaving nulls untouched
|
|
79
|
+
is_anomaly = val_scaled.is_nan() | val_scaled.is_infinite()
|
|
80
|
+
safe_val = _when(is_anomaly).then(_lit(0.0)).otherwise(val_scaled)
|
|
93
81
|
|
|
94
|
-
# 2. Dynamic Precision Handling (Significant Figures vs Fixed Decimals)
|
|
95
82
|
if n_sigfig is not None:
|
|
96
83
|
if n_sigfig < 1:
|
|
97
84
|
raise ValueError('n_sigfig must be a positive integer >= 1')
|
|
98
85
|
|
|
99
|
-
|
|
100
|
-
rounded = val_scaled.round_sig_figs(n_sigfig)
|
|
86
|
+
rounded = safe_val.round_sig_figs(n_sigfig)
|
|
101
87
|
abs_rounded = rounded.abs()
|
|
102
88
|
|
|
103
|
-
# Determine the number of dynamic decimal places needed for padding per row
|
|
104
89
|
log10_expr = (
|
|
105
90
|
_when(abs_rounded > 0)
|
|
106
91
|
.then(abs_rounded.log10())
|
|
107
92
|
.otherwise(_lit(0.0))
|
|
108
93
|
)
|
|
109
|
-
# decimals required = n_sigfig - 1 - floor(log10(x))
|
|
110
94
|
dynamic_decimals = (_lit(n_sigfig - 1) - log10_expr.floor()).cast(
|
|
111
95
|
_Int32
|
|
112
96
|
)
|
|
113
|
-
# Ensure we don't try to pad negative decimal places for large numbers
|
|
114
97
|
dynamic_decimals = (
|
|
115
98
|
_when(dynamic_decimals < 0)
|
|
116
99
|
.then(_lit(0))
|
|
117
100
|
.otherwise(dynamic_decimals)
|
|
118
101
|
)
|
|
119
102
|
else:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
_when(val_scaled >= 0).then(_lit(1e-9)).otherwise(_lit(-1e-9))
|
|
123
|
-
)
|
|
124
|
-
rounded = (val_scaled + epsilon).round(decimals)
|
|
103
|
+
epsilon = _when(safe_val >= 0).then(_lit(1e-9)).otherwise(_lit(-1e-9))
|
|
104
|
+
rounded = (safe_val + epsilon).round(decimals)
|
|
125
105
|
dynamic_decimals = _lit(decimals)
|
|
126
106
|
|
|
127
|
-
|
|
107
|
+
return rounded, dynamic_decimals
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _extract_and_align_components(
|
|
111
|
+
rounded: _Expr,
|
|
112
|
+
decimals: int,
|
|
113
|
+
n_sigfig: int | None,
|
|
114
|
+
dynamic_decimals: _Expr | int,
|
|
115
|
+
use_seps: bool,
|
|
116
|
+
) -> _Expr:
|
|
117
|
+
"""Handles logic blocks 3 & 4: Cuts string tokens cleanly."""
|
|
118
|
+
# Since rounded is calculated from safe_val above, it contains no NaN/inf here.
|
|
119
|
+
# The .cast(_Int64) is now completely safe from panicking!
|
|
128
120
|
int_part = rounded.cast(_Int64).abs().cast(_String)
|
|
129
121
|
full_str = rounded.abs().cast(_String)
|
|
130
122
|
|
|
131
|
-
raw_frac = (
|
|
132
|
-
_when(full_str.str.contains(r'\.'))
|
|
133
|
-
.then(full_str.str.split('.').list.get(1))
|
|
134
|
-
.otherwise(_lit(''))
|
|
135
|
-
)
|
|
123
|
+
raw_frac = full_str.str.extract(r'\.(.*)', 1).fill_null(_lit(''))
|
|
136
124
|
|
|
137
|
-
# Use native Python string multiplication inside _lit()
|
|
138
125
|
pad_len = n_sigfig if n_sigfig is not None else decimals
|
|
139
126
|
frac_part = (
|
|
140
127
|
_when(dynamic_decimals > 0)
|
|
@@ -142,7 +129,6 @@ def fmt_number(
|
|
|
142
129
|
.otherwise(_lit(''))
|
|
143
130
|
)
|
|
144
131
|
|
|
145
|
-
# 4. Constructing Base Number String
|
|
146
132
|
if use_seps:
|
|
147
133
|
int_part = (
|
|
148
134
|
int_part.str.reverse()
|
|
@@ -151,19 +137,56 @@ def fmt_number(
|
|
|
151
137
|
.str.reverse()
|
|
152
138
|
)
|
|
153
139
|
|
|
154
|
-
|
|
140
|
+
return (
|
|
155
141
|
_when(dynamic_decimals > 0)
|
|
156
142
|
.then(int_part + _lit('.') + frac_part)
|
|
157
143
|
.otherwise(int_part)
|
|
158
144
|
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@_multicolumn
|
|
148
|
+
def fmt_number(
|
|
149
|
+
*,
|
|
150
|
+
columns: _Columns,
|
|
151
|
+
decimals: int = 2,
|
|
152
|
+
scale_by: float = 1.0,
|
|
153
|
+
compact: bool = False,
|
|
154
|
+
compact_system: _Literal['financial', 'engineering'] = 'financial',
|
|
155
|
+
use_seps: bool = True,
|
|
156
|
+
accounting: bool = False,
|
|
157
|
+
pattern: str = '{x}',
|
|
158
|
+
n_sigfig: int | None = None,
|
|
159
|
+
force_sign: bool = False,
|
|
160
|
+
) -> _Expr:
|
|
161
|
+
"""
|
|
162
|
+
Highly optimized, native Polars numeric formatter matching great_tables features.
|
|
163
|
+
Runs entirely in the parallel Rust engine without dropping into Python row loops.
|
|
164
|
+
"""
|
|
165
|
+
col_name = columns if isinstance(columns, str) else str(columns)
|
|
166
|
+
val = _col(columns)
|
|
167
|
+
if scale_by != 1.0:
|
|
168
|
+
val = val * scale_by
|
|
169
|
+
|
|
170
|
+
# --- HELPER ORCHESTRATION ---
|
|
171
|
+
val_scaled, suffix_chain = _apply_compact_scaling(
|
|
172
|
+
val, compact, compact_system
|
|
173
|
+
)
|
|
174
|
+
rounded, dynamic_decimals = _compute_dynamic_precision(
|
|
175
|
+
val_scaled, decimals, n_sigfig
|
|
176
|
+
)
|
|
177
|
+
base_num_str = _extract_and_align_components(
|
|
178
|
+
rounded, decimals, n_sigfig, dynamic_decimals, use_seps
|
|
179
|
+
)
|
|
159
180
|
base_num_str = base_num_str + suffix_chain
|
|
160
181
|
|
|
182
|
+
# --- PATTERN PARSING ---
|
|
161
183
|
prefix, suffix = '', ''
|
|
162
184
|
if pattern != '{x}':
|
|
163
185
|
parts = pattern.split('{x}')
|
|
164
186
|
if len(parts) == 2:
|
|
165
187
|
prefix, suffix = parts[0], parts[1]
|
|
166
188
|
|
|
189
|
+
# --- STANDARD EXPRESSION GENERATION ---
|
|
167
190
|
if accounting:
|
|
168
191
|
formatted_expr = (
|
|
169
192
|
_when(val < 0)
|
|
@@ -175,7 +198,6 @@ def fmt_number(
|
|
|
175
198
|
+ _lit(')')
|
|
176
199
|
)
|
|
177
200
|
.otherwise(
|
|
178
|
-
# If force_sign is True, explicitly prepend '+' to positive values
|
|
179
201
|
_lit('+' if force_sign else '')
|
|
180
202
|
+ _lit(prefix)
|
|
181
203
|
+ base_num_str
|
|
@@ -186,13 +208,22 @@ def fmt_number(
|
|
|
186
208
|
formatted_expr = (
|
|
187
209
|
_when(val < 0)
|
|
188
210
|
.then(_lit('-') + _lit(prefix) + base_num_str + _lit(suffix))
|
|
189
|
-
# Handle zero explicitly if you don't want a sign on exactly 0.0
|
|
190
211
|
.when((val == 0) | (not force_sign))
|
|
191
212
|
.then(_lit(prefix) + base_num_str + _lit(suffix))
|
|
192
213
|
.otherwise(_lit('+') + _lit(prefix) + base_num_str + _lit(suffix))
|
|
193
214
|
)
|
|
194
215
|
|
|
195
|
-
|
|
216
|
+
# --- CRUCIAL FIX: NON-FINITE GLOBAL GUARD MASK ---
|
|
217
|
+
# Intercepts expression evaluation to bypass Int64 panics on NaN and Infinity tokens
|
|
218
|
+
final_guarded_expr = (
|
|
219
|
+
_when(val.is_nan())
|
|
220
|
+
.then(_lit('NaN'))
|
|
221
|
+
.when(val.is_infinite())
|
|
222
|
+
.then(_when(val < 0).then(_lit('-inf')).otherwise(_lit('inf')))
|
|
223
|
+
.otherwise(formatted_expr)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return final_guarded_expr.alias(col_name)
|
|
196
227
|
|
|
197
228
|
|
|
198
229
|
@_multicolumn
|
|
File without changes
|