Skip to content

Commit 92e747b

Browse files
Iteration 159: Add format_ops — number formatting for Series and DataFrame
Implements 14 formatting functions in src/stats/format_ops.ts: - Scalar formatters: formatFloat, formatPercent, formatScientific, formatEngineering, formatThousands, formatCurrency, formatCompact - Formatter factories: makeFloatFormatter, makePercentFormatter, makeCurrencyFormatter - Apply helpers: applySeriesFormatter (→ Series<string>), applyDataFrameFormatter (→ Record<string, string[]>) - String rendering: seriesToString, dataFrameToString 84 tests all pass (unit + property-based). 100% line + func coverage. Playground page added at playground/format_ops.html. Metric: 88 → 89 (pandas_features_ported). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0c05e67 commit 92e747b

6 files changed

Lines changed: 1321 additions & 0 deletions

File tree

playground/format_ops.html

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>tsb — format_ops: Number Formatting</title>
7+
<style>
8+
body { font-family: system-ui, sans-serif; max-width: 860px; margin: 2rem auto; padding: 0 1rem; color: #1a1a2e; }
9+
h1 { color: #16213e; }
10+
h2 { color: #0f3460; border-bottom: 2px solid #e94560; padding-bottom: 0.25rem; }
11+
pre { background: #f4f4f8; border-left: 4px solid #e94560; padding: 1rem; overflow-x: auto; border-radius: 4px; }
12+
code { font-family: 'Fira Code', 'Cascadia Code', monospace; font-size: 0.9em; }
13+
.demo { background: #eef2ff; border-radius: 6px; padding: 1rem 1.25rem; margin: 1rem 0; }
14+
.demo label { font-weight: 600; display: block; margin-bottom: 0.3rem; }
15+
.demo input, .demo select { padding: 0.3rem 0.5rem; border: 1px solid #aaa; border-radius: 4px; margin-right: 0.5rem; }
16+
.demo button { background: #e94560; color: white; border: none; padding: 0.4rem 0.9rem; border-radius: 4px; cursor: pointer; }
17+
.demo button:hover { background: #c73652; }
18+
.output { font-family: monospace; background: white; border: 1px solid #ccc; padding: 0.75rem; border-radius: 4px; margin-top: 0.5rem; min-height: 2rem; white-space: pre; }
19+
table { border-collapse: collapse; width: 100%; }
20+
th, td { border: 1px solid #ccc; padding: 0.5rem 0.75rem; text-align: left; }
21+
th { background: #16213e; color: white; }
22+
tr:nth-child(even) { background: #f8f8fc; }
23+
a { color: #e94560; }
24+
</style>
25+
</head>
26+
<body>
27+
<h1>🔢 <code>format_ops</code> — Number Formatting</h1>
28+
<p>
29+
<strong>tsb</strong> provides a suite of number-formatting helpers that mirror pandas'
30+
<code>style.format()</code> and <code>Series.map()</code> patterns.
31+
Every function is zero-dependency and fully typed.
32+
</p>
33+
<p><a href="index.html">← Back to index</a></p>
34+
35+
<h2>Scalar formatters</h2>
36+
37+
<table>
38+
<thead><tr><th>Function</th><th>Example input</th><th>Example output</th><th>Notes</th></tr></thead>
39+
<tbody>
40+
<tr><td><code>formatFloat(n, d)</code></td><td><code>3.14159, 2</code></td><td><code>"3.14"</code></td><td>Fixed decimal places</td></tr>
41+
<tr><td><code>formatPercent(n, d)</code></td><td><code>0.1234, 1</code></td><td><code>"12.3%"</code></td><td>Multiplies by 100</td></tr>
42+
<tr><td><code>formatScientific(n, d)</code></td><td><code>12345.678, 3</code></td><td><code>"1.235e+4"</code></td><td>Exponential notation</td></tr>
43+
<tr><td><code>formatEngineering(n, d)</code></td><td><code>12345.678, 3</code></td><td><code>"12.346e+3"</code></td><td>Exponent multiple of 3</td></tr>
44+
<tr><td><code>formatThousands(n, d, sep)</code></td><td><code>1234567.89, 2</code></td><td><code>"1,234,567.89"</code></td><td>Thousands separator</td></tr>
45+
<tr><td><code>formatCurrency(n, sym, d)</code></td><td><code>1234.5, "$"</code></td><td><code>"$1,234.50"</code></td><td>Currency prefix + thousands</td></tr>
46+
<tr><td><code>formatCompact(n, d)</code></td><td><code>1_234_567, 2</code></td><td><code>"1.23M"</code></td><td>K / M / B / T suffixes</td></tr>
47+
</tbody>
48+
</table>
49+
50+
<h2>Interactive demo — scalar formatting</h2>
51+
<div class="demo">
52+
<label>Number: <input id="numInput" type="number" value="1234567.89" style="width:180px" /></label>
53+
<label>Format:
54+
<select id="fmtSelect">
55+
<option value="float">formatFloat(n, decimals)</option>
56+
<option value="percent">formatPercent(n, decimals)</option>
57+
<option value="scientific">formatScientific(n, decimals)</option>
58+
<option value="engineering">formatEngineering(n, decimals)</option>
59+
<option value="thousands">formatThousands(n, decimals)</option>
60+
<option value="currency">formatCurrency(n, "$", decimals)</option>
61+
<option value="compact">formatCompact(n, decimals)</option>
62+
</select>
63+
</label>
64+
<label>Decimals: <input id="decInput" type="number" value="2" min="0" max="10" style="width:60px" /></label>
65+
<button onclick="runFmt()">Format</button>
66+
<div class="output" id="fmtOutput"></div>
67+
</div>
68+
69+
<h2>Formatter factories</h2>
70+
<pre><code>import {
71+
makeFloatFormatter,
72+
makePercentFormatter,
73+
makeCurrencyFormatter,
74+
} from "tsb";
75+
76+
const fmtFloat = makeFloatFormatter(3); // (v) => formatFloat(v, 3)
77+
const fmtPct = makePercentFormatter(1); // (v) => formatPercent(v, 1)
78+
const fmtDollar = makeCurrencyFormatter("$"); // (v) => formatCurrency(v, "$", 2)
79+
80+
fmtFloat(3.14159); // "3.142"
81+
fmtPct(0.0825); // "8.3%"
82+
fmtDollar(9999.99); // "$9,999.99"
83+
</code></pre>
84+
85+
<h2>Apply to a Series</h2>
86+
<pre><code>import { Series, applySeriesFormatter, makePercentFormatter } from "tsb";
87+
88+
const returns = new Series({ data: [0.05, -0.02, 0.134, 0.007], name: "returns" });
89+
90+
const formatted = applySeriesFormatter(returns, makePercentFormatter(1));
91+
// Series&lt;string&gt; ["5.0%", "-2.0%", "13.4%", "0.7%"]
92+
</code></pre>
93+
94+
<h2>Apply to a DataFrame</h2>
95+
<pre><code>import { DataFrame, applyDataFrameFormatter, makeCurrencyFormatter, makePercentFormatter } from "tsb";
96+
97+
const df = DataFrame.fromColumns({
98+
price: [1_299.99, 899.50, 45.00],
99+
change: [0.025, -0.031, 0.102],
100+
volume: [15_000, 8_200, 230_000],
101+
});
102+
103+
const formatted = applyDataFrameFormatter(df, {
104+
price: makeCurrencyFormatter("$", 2),
105+
change: makePercentFormatter(2),
106+
});
107+
108+
// formatted = {
109+
// price: ["$1,299.99", "$899.50", "$45.00"],
110+
// change: ["2.50%", "-3.10%", "10.20%"],
111+
// volume: ["15000", "8200", "230000"], // no formatter → String(v)
112+
// }
113+
</code></pre>
114+
115+
<h2>Interactive demo — DataFrame formatting</h2>
116+
<div class="demo">
117+
<button onclick="runDfFmt()">Run DataFrame example</button>
118+
<div class="output" id="dfOutput"></div>
119+
</div>
120+
121+
<h2>String rendering</h2>
122+
<pre><code>import { Series, DataFrame, seriesToString, dataFrameToString, makeFloatFormatter } from "tsb";
123+
124+
const s = new Series({ data: [1.2, 3.4, 5.6], name: "value" });
125+
console.log(seriesToString(s, { formatter: makeFloatFormatter(1) }));
126+
// 0 1.2
127+
// 1 3.4
128+
// 2 5.6
129+
// Name: value, dtype: float64
130+
131+
const df = DataFrame.fromColumns({ a: [1, 2, 3], b: [4.0, 5.0, 6.0] });
132+
console.log(dataFrameToString(df));
133+
// a b
134+
// 0 1 4.0
135+
// 1 2 5.0
136+
// 2 3 6.0
137+
</code></pre>
138+
139+
<h2>Interactive demo — seriesToString / dataFrameToString</h2>
140+
<div class="demo">
141+
<button onclick="runToString()">Run toString example</button>
142+
<div class="output" id="tsOutput"></div>
143+
</div>
144+
145+
<script type="module">
146+
// Inline implementations for the playground (mirrors the real tsb exports)
147+
148+
function formatFloat(value, decimals = 2) {
149+
if (!Number.isFinite(value)) return String(value);
150+
return value.toFixed(decimals);
151+
}
152+
function formatPercent(value, decimals = 2) {
153+
if (!Number.isFinite(value)) return String(value);
154+
return `${(value * 100).toFixed(decimals)}%`;
155+
}
156+
function formatScientific(value, decimals = 3) {
157+
if (!Number.isFinite(value)) return String(value);
158+
return value.toExponential(decimals);
159+
}
160+
function formatEngineering(value, decimals = 3) {
161+
if (!Number.isFinite(value)) return String(value);
162+
if (value === 0) return `0.${"0".repeat(decimals)}e+0`;
163+
const sign = value < 0 ? "-" : "";
164+
const abs = Math.abs(value);
165+
const exp = Math.floor(Math.log10(abs));
166+
const engExp = Math.floor(exp / 3) * 3;
167+
const mantissa = abs / 10 ** engExp;
168+
const expSign = engExp >= 0 ? "+" : "-";
169+
return `${sign}${mantissa.toFixed(decimals)}e${expSign}${Math.abs(engExp)}`;
170+
}
171+
function formatThousands(value, decimals = 2, separator = ",") {
172+
if (!Number.isFinite(value)) return String(value);
173+
const fixed = value.toFixed(decimals);
174+
const [intPart, fracPart] = fixed.split(".");
175+
const intStr = intPart ?? "";
176+
const isNeg = intStr.startsWith("-");
177+
const digits = isNeg ? intStr.slice(1) : intStr;
178+
const withSep = digits.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
179+
const sign = isNeg ? "-" : "";
180+
return fracPart !== undefined ? `${sign}${withSep}.${fracPart}` : `${sign}${withSep}`;
181+
}
182+
function formatCurrency(value, symbol = "$", decimals = 2) {
183+
if (!Number.isFinite(value)) return `${symbol}${String(value)}`;
184+
const abs = Math.abs(value);
185+
const sign = value < 0 ? "-" : "";
186+
return `${sign}${symbol}${formatThousands(abs, decimals)}`;
187+
}
188+
function formatCompact(value, decimals = 2) {
189+
if (!Number.isFinite(value)) return String(value);
190+
const sign = value < 0 ? "-" : "";
191+
const abs = Math.abs(value);
192+
if (abs >= 1e12) return `${sign}${(abs / 1e12).toFixed(decimals)}T`;
193+
if (abs >= 1e9) return `${sign}${(abs / 1e9).toFixed(decimals)}B`;
194+
if (abs >= 1e6) return `${sign}${(abs / 1e6).toFixed(decimals)}M`;
195+
if (abs >= 1e3) return `${sign}${(abs / 1e3).toFixed(decimals)}K`;
196+
return `${sign}${abs.toFixed(decimals)}`;
197+
}
198+
199+
window.runFmt = function() {
200+
const n = parseFloat(document.getElementById("numInput").value);
201+
const d = parseInt(document.getElementById("decInput").value, 10);
202+
const fmt = document.getElementById("fmtSelect").value;
203+
let result;
204+
switch (fmt) {
205+
case "float": result = formatFloat(n, d); break;
206+
case "percent": result = formatPercent(n, d); break;
207+
case "scientific": result = formatScientific(n, d); break;
208+
case "engineering": result = formatEngineering(n, d); break;
209+
case "thousands": result = formatThousands(n, d); break;
210+
case "currency": result = formatCurrency(n, "$", d); break;
211+
case "compact": result = formatCompact(n, d); break;
212+
default: result = "?";
213+
}
214+
document.getElementById("fmtOutput").textContent = `"${result}"`;
215+
};
216+
217+
window.runDfFmt = function() {
218+
const data = [
219+
{ price: 1299.99, change: 0.025, volume: 15000 },
220+
{ price: 899.50, change: -0.031, volume: 8200 },
221+
{ price: 45.00, change: 0.102, volume: 230000 },
222+
];
223+
const cols = ["price", "change", "volume"];
224+
const fmts = {
225+
price: (v) => formatCurrency(v, "$", 2),
226+
change: (v) => formatPercent(v, 2),
227+
};
228+
const lines = [cols.map(c => c.padStart(12)).join("")];
229+
data.forEach((row, i) => {
230+
const cells = cols.map(c => {
231+
const fmt = fmts[c] ?? String;
232+
return fmt(row[c]).padStart(12);
233+
});
234+
lines.push(`${i} ${cells.join("")}`);
235+
});
236+
document.getElementById("dfOutput").textContent = lines.join("\n");
237+
};
238+
239+
window.runToString = function() {
240+
const data = [0.05, -0.02, 0.134, 0.007, 0.089];
241+
const lines = [];
242+
lines.push("=== seriesToString (percent formatter) ===");
243+
data.forEach((v, i) => {
244+
lines.push(`${String(i).padEnd(3)} ${formatPercent(v, 1).padStart(8)}`);
245+
});
246+
lines.push("Name: returns, dtype: float64");
247+
lines.push("");
248+
lines.push("=== dataFrameToString ===");
249+
const df = [
250+
{ a: 1, b: 4.0 },
251+
{ a: 2, b: 5.0 },
252+
{ a: 3, b: 6.0 },
253+
];
254+
lines.push(" a b");
255+
df.forEach((row, i) => {
256+
lines.push(`${i} ${String(row.a).padEnd(4)} ${String(row.b)}`);
257+
});
258+
document.getElementById("tsOutput").textContent = lines.join("\n");
259+
};
260+
</script>
261+
</body>
262+
</html>

playground/index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,13 @@ <h3><a href="categorical_ops.html" style="color: var(--accent); text-decoration:
327327
<div class="status done">✅ Complete</div>
328328
</div>
329329
</div>
330+
<div class="feature-card done">
331+
<div class="feature-content">
332+
<h3><a href="format_ops.html" style="color: var(--accent); text-decoration: none;">🔢 format_ops — Number Formatting</a></h3>
333+
<p>Number-formatting helpers for Series and DataFrame. Scalar formatters: <code>formatFloat</code>, <code>formatPercent</code>, <code>formatScientific</code>, <code>formatEngineering</code>, <code>formatThousands</code>, <code>formatCurrency</code>, <code>formatCompact</code>. Formatter factories: <code>makeFloatFormatter</code>, <code>makePercentFormatter</code>, <code>makeCurrencyFormatter</code>. Apply to collections: <code>applySeriesFormatter</code>, <code>applyDataFrameFormatter</code>. Render to string: <code>seriesToString</code>, <code>dataFrameToString</code>.</p>
334+
<div class="status done">✅ Complete</div>
335+
</div>
336+
</div>
330337
</section>
331338
</main>
332339

src/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,24 @@ export type {
255255
CatSortByFreqOptions,
256256
CatCrossTabOptions,
257257
} from "./stats/index.ts";
258+
export {
259+
formatFloat,
260+
formatPercent,
261+
formatScientific,
262+
formatEngineering,
263+
formatThousands,
264+
formatCurrency,
265+
formatCompact,
266+
makeFloatFormatter,
267+
makePercentFormatter,
268+
makeCurrencyFormatter,
269+
applySeriesFormatter,
270+
applyDataFrameFormatter,
271+
seriesToString,
272+
dataFrameToString,
273+
} from "./stats/index.ts";
274+
export type {
275+
Formatter,
276+
SeriesToStringOptions,
277+
DataFrameToStringOptions,
278+
} from "./stats/index.ts";

0 commit comments

Comments
 (0)