PolyTools Trade Cost 0.1.1 / src/lib/format.js

97 lines · 4,068 bytes · SHA-256 8ee789785171ed87ef5784b780810d51fca8800cdaaf9c864ed81068f8bc46d9

1/* PolyTools — number/date formatting (locale-aware, no DOM). */
2(function (root) {
3 'use strict';
4
5 let locale = 'en-US';
6 const MINUS = '−';
7 const NA = '-'; // placeholder for a missing value (plain hyphen, no long dashes in UI text)
8
9 const LOCALES = { en: 'en-US', ru: 'ru-RU', es: 'es-ES', zh: 'zh-CN', fr: 'fr-FR' };
10 const PP = { en: ' pp', ru: ' п.п.', es: ' p. p.', zh: ' 个百分点', fr: ' pts' };
11 function setLocale(lang) { locale = LOCALES[lang] || 'en-US'; }
12
13 const nf = (min, max) => new Intl.NumberFormat(locale, { minimumFractionDigits: min, maximumFractionDigits: max });
14
15 /** Sign follows what is displayed: a value that rounds to zero never shows as "−0%" or "+$0.00". */
16 function signed(str, x, sign) {
17 if (!/[1-9]/.test(str)) return str;
18 // U+2060 WORD JOINER keeps the sign glued to the number when a line wraps ("−" / "$1,845" on two lines)
19 if (x < 0) return MINUS + '\u2060' + str;
20 if (sign && x > 0) return '+\u2060' + str;
21 return str;
22 }
23
24 /** $1,234.56 ; compact: $12.9K / $4.2M ; axis: $500 / $5K (no cents) ; sign: +$ / −$ */
25 function money(x, o) {
26 if (x == null || !Number.isFinite(x)) return NA;
27 const opts = o || {};
28 const a = Math.abs(x);
29 let body;
30 if (opts.axis) {
31 body = a >= 1000
32 ? new Intl.NumberFormat(locale, { notation: 'compact', maximumFractionDigits: 1 }).format(a)
33 : nf(0, a < 10 && a % 1 ? 2 : 0).format(a);
34 } else if (opts.compact && a >= 10000) {
35 body = new Intl.NumberFormat(locale, { notation: 'compact', maximumFractionDigits: a >= 1e6 ? 2 : 1 }).format(a);
36 } else {
37 const d = opts.digits != null ? opts.digits : (a > 0 && a < 1 ? 3 : 2);
38 body = nf(Math.min(d, 2), d).format(a);
39 }
40 return signed('$' + body, x, opts.sign);
41 }
42
43 /** Price in cents: 0.964 → 96.4¢ */
44 function cents(p, digits) {
45 if (p == null || !Number.isFinite(p)) return NA;
46 return nf(0, digits != null ? digits : 2).format(p * 100) + '¢';
47 }
48
49 /** 0.0373 → 3.73% */
50 function pct(x, digits, sign) {
51 if (x == null || !Number.isFinite(x)) return NA;
52 const a = Math.abs(x * 100);
53 const d = digits != null ? digits : (a >= 100 ? 0 : a >= 10 ? 1 : 2);
54 return signed(nf(0, d).format(a) + '%', x, sign);
55 }
56
57 /** percentage points: 0.052 → +5.2 pp */
58 function pp(x, lang) {
59 if (x == null || !Number.isFinite(x)) return NA;
60 return signed(nf(0, 1).format(Math.abs(x * 100)), x, true) + (PP[lang] || PP.en);
61 }
62
63 function number(x, digits) {
64 if (x == null || !Number.isFinite(x)) return NA;
65 return signed(nf(0, digits != null ? digits : 2).format(Math.abs(x)), x, false);
66 }
67
68 function int(x) { return x == null || !Number.isFinite(x) ? NA : new Intl.NumberFormat(locale).format(Math.round(x)); }
69
70 function date(tsSec) {
71 if (!(tsSec > 0)) return NA;
72 return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'short', day: 'numeric' }).format(new Date(tsSec * 1000));
73 }
74
75 function dateIso(iso) {
76 const t = Date.parse(iso);
77 if (!Number.isFinite(t)) return NA;
78 return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'short', day: 'numeric' }).format(new Date(t));
79 }
80
81 function month(key) {
82 const [y, m] = key.split('-').map(Number);
83 return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'short', timeZone: 'UTC' }).format(new Date(Date.UTC(y, m - 1, 1)));
84 }
85
86 /** 0.4 d → 9.6 h ; 3.2 d → 3.2 d (labels supplied by i18n) */
87 function duration(days, t) {
88 if (days == null || !Number.isFinite(days)) return NA;
89 if (days < 1) return nf(0, 1).format(days * 24) + ' ' + t('unit.h');
90 return nf(0, days < 10 ? 1 : 0).format(days) + ' ' + t('unit.d');
91 }
92
93 const fmt = { NA, setLocale, get locale() { return locale; }, money, cents, pct, pp, number, int, date, dateIso, month, duration, MINUS };
94 root.PWL = root.PWL || {};
95 root.PWL.fmt = fmt;
96 if (typeof module !== 'undefined' && module.exports) module.exports = fmt;
97})(typeof globalThis !== 'undefined' ? globalThis : this);