UNPKG

@technobuddha/library

Version:
465 lines (464 loc) 15.1 kB
import defaultTo from 'lodash/defaultTo'; import map from 'lodash/map'; import { empty } from '../constants'; import padNumber from '../padNumber'; import splitChars from '../splitChars'; import build from '../build'; function parse(mask) { let scale = 1; let beforeDP = true; const before = []; let after = []; let group = false; let commas = 0; let zeroSeen = false; let exponent = 0; let signExponent = false; let precision = 0; const m = splitChars(mask); for (let i = 0; i < m.length; ++i) { const c = m[i]; switch (c) { case '"': //literal string case "'": { let s = '"'; for (++i; i < mask.length; ++i) { const k = mask.charAt(i); if (c === k) break; s += k; } (beforeDP ? before : after).push(s); break; } case '#': { if (beforeDP) { before.push(zeroSeen ? '0' : '#'); } else { precision++; after.push('#'); } break; } case '0': { if (beforeDP) { //If we see a 0 before the decimal point, all following #s are transformed into 0s before.push('0'); zeroSeen = true; } else { //if we see a 0 after the decimal point, the proceeding #s are transformed into 0s precision++; after = map(after, a => (a === '#' ? '0' : a)); after.push('0'); } break; } case ',': { if (beforeDP) commas++; break; } case '.': { beforeDP = false; break; } case '%': { scale *= 100; (beforeDP ? before : after).push(`"${c}`); break; } case '‰': { scale *= 1000; (beforeDP ? before : after).push(`"${c}`); break; } case '‱': { scale *= 10000; (beforeDP ? before : after).push(`"${c}`); break; } case 'e': case 'E': { let signSeen = false; let j = i + 1; let e = 0; if (mask.length > j && mask.charAt(j) === '+') { j++; signSeen = true; } else if (mask.length > j && mask.charAt(j) === '-') { j++; } while (mask.length > j && mask.charAt(j) === '0') { j++; e++; } if (e > 0) { i = j - 1; exponent = e; signExponent = signSeen; (beforeDP ? before : after).push(c); } else { (beforeDP ? before : after).push(`"${c}`); } break; } case '\\': { if (i < mask.length - 1) (beforeDP ? before : after).push(`"${mask.charAt(++i)}`); else (beforeDP ? before : after).push('"\\'); break; } default: { (beforeDP ? before : after).push(`"${c}`); break; } } if (beforeDP && c !== ',') { if (commas > 0) group = true; commas = 0; } } scale = scale / Math.pow(1000, commas); return { aMask: after, aDigits: after.reduce((acc, val) => ((val === '0' || val === '#') ? acc + 1 : acc), 0), bMask: before, bDigits: before.reduce((acc, val) => ((val === '0' || val === '#') ? acc + 1 : acc), 0), scale: scale, group: group, exponent: exponent, signExponent: signExponent, precision: precision, }; } function format(input, { round, precision, scale, lead = 1, trim = 'none' }) { const sign = Math.sign(input); const [m, e] = Math.abs(input).toExponential(15).split('e'); let exponent = Number(e) + 1; // +1 because we store the number without the decimal point const mantissa = m.replace('.', empty).split(empty); while (mantissa.length > exponent && mantissa[mantissa.length - 1] === '0') --mantissa.length; const rounder = (n) => { if (mantissa.length < n) { while (mantissa.length < n) mantissa.push('0'); } else { const c = mantissa[n]; mantissa.length = n; if (c > '4') { for (;;) { if (n < 0) { mantissa.unshift('0'); ++exponent; n = 1; } const d = mantissa[--n]; if (d === '0') { mantissa[n] = '1'; break; } if (d === '1') { mantissa[n] = '2'; break; } if (d === '2') { mantissa[n] = '3'; break; } if (d === '3') { mantissa[n] = '4'; break; } if (d === '4') { mantissa[n] = '5'; break; } if (d === '5') { mantissa[n] = '6'; break; } if (d === '6') { mantissa[n] = '7'; break; } if (d === '7') { mantissa[n] = '8'; break; } if (d === '8') { mantissa[n] = '9'; break; } if (d === '9') { mantissa[n] = '0'; mantissa.unshift('1'); ++exponent; break; } process.stderr.write(`"${d}"${n}"`); } } } }; if (scale !== undefined) exponent += scale; if (round !== undefined) rounder(exponent + round); if (precision !== undefined) rounder(precision); let length = Math.min(exponent, mantissa.length); while (length < lead) { mantissa.unshift('0'); ++exponent; ++length; } if (trim === 'front' || trim === 'all') { while (mantissa.length > 1 && mantissa[0] === '0') { mantissa.shift(); --exponent; } } if (trim === 'back' || trim === 'all') { while (mantissa.length > exponent && mantissa[mantissa.length - 1] === '0') --mantissa.length; } return new NumberFormatter(sign, mantissa, exponent); } class NumberFormatter { sign; mantissa; exponent; constructor(sign, mantissa, exponent) { this.sign = sign; this.mantissa = mantissa; this.exponent = exponent; this.output = []; } output; minus(negative, positive = empty) { this.output.push(this.sign < 0 ? negative : positive); return this; } grouped() { const whole = this.mantissa.slice(0, this.exponent); this.output.push(whole.map((c, i) => (i > 0 && (whole.length - i) % 3 === 0 ? `,${c}` : c))); return this; } whole() { const whole = this.mantissa.slice(0, this.exponent); while (whole.length < this.exponent) whole.push('0'); this.output.push(whole); return this; } decimal() { if (this.exponent < this.mantissa.length) this.output.push('.'); return this; } fraction() { this.output.push(this.mantissa.slice(this.exponent)); return this; } text(str) { this.output.push(str); return this; } scientific(e) { this.output.push(this.mantissa[0], '.', this.mantissa.slice(1), e, this.exponent > 0 ? '+' : empty, padNumber(this.exponent - 1, 3)); return this; } build() { return build(...this.output); } } //#endregion //#region formatNumber export function formatNumber(input, mask) { if (/^([CDEFGNPX][0-9]*)|R$/ui.test(mask)) { const f = mask.charAt(0); let prec = Number.parseInt(mask.slice(1), 10); switch (f) { case 'C': case 'c': { prec = defaultTo(prec, 2); return format(input, { round: prec, lead: 1 }).minus('($', '$').grouped().decimal().fraction().minus(')').build(); } case 'D': case 'd': { prec = defaultTo(prec, 2); return format(input, { round: 0, lead: prec }).minus('-').whole().build(); } case 'E': case 'e': { prec = defaultTo(prec, 6); return format(input, { precision: prec + 1 }).minus('-').scientific(f).build(); } case 'F': case 'f': { prec = defaultTo(prec, 2); return format(input, { round: prec }).minus('-').whole().decimal().fraction().build(); } case 'G': case 'g': { prec = defaultTo(prec, 15); const sci = format(input, { precision: prec, trim: 'all' }).minus('-').scientific(f === 'G' ? 'E' : 'e').build(); const fix = format(input, { precision: prec, trim: 'back' }).minus('-').whole().decimal().fraction().build(); return sci.length < fix.length ? sci : fix; } case 'N': case 'n': { prec = defaultTo(prec, 2); return format(input, { round: prec }).minus('-').grouped().decimal().fraction().build(); } case 'P': case 'p': { prec = defaultTo(prec, 2); return format(input, { scale: 2, round: prec }).minus('-').whole().decimal().fraction().text(' %').build(); } case 'R': case 'r': { for (let i = 1; i < 21; ++i) { const num = input.toPrecision(i); if (Number.parseFloat(num) === input) return num; } break; } // No default } prec = defaultTo(prec, 0); let hex = (input >>> 0).toString(16); hex = hex.padStart(prec, '0'); if (f === 'X') hex = hex.toUpperCase(); return hex; } const formats = mask.toString().split(';'); let fmt = parse(formats[0]); if (Number.parseFloat((input * fmt.scale).toFixed(fmt.precision)) === 0) fmt = formats.length < 3 ? fmt : parse(formats[2]); else if (input < 0) fmt = formats.length < 2 ? parse(`-${formats[0]}`) : parse(formats[1]); let w; let f; let exp = 0; if (fmt.exponent > 0) { const [m, e] = Math.abs(input).toExponential(fmt.aDigits + fmt.bDigits - 1).split('e'); ([w, f] = m.split('.').map(x => x.split(empty))); exp = Number(e); while (w.length < fmt.bDigits) { w.push(f.shift()); exp--; } } else if (fmt.bDigits === 0) { w = (Math.abs(input * fmt.scale)).toFixed(0).split(empty); f = []; } else { let scaled = Math.abs(input * fmt.scale); let rescale = 0; let str; let split; //toFixed for numbers greater than 1e21 return scientific notation... while (scaled > 1e21) { scaled /= 1e21; rescale += 21; } if (rescale > 0) { str = (scaled).toFixed(17); split = str.split('.'); w = split[0].split(empty); f = split[1].split(empty); while (rescale-- > 0) { w.push(f.shift()); f.push('0'); } } else { str = (scaled).toFixed(fmt.aDigits); split = str.split('.'); w = split[0].split(empty); //whole part f = split.length > 1 ? split[1].split(empty) : []; //fractional } } while (w.length > 0 && w[0] === '0') w.shift(); let o = empty; let d = 0; let b = fmt.bDigits; for (let i = fmt.bMask.length - 1; i >= 0; --i) { const x = fmt.bMask[i]; if (x === '0' || x === '#') { if (fmt.group && d === 3 && w.length > 0) { o = `,${o}`; d = 0; } if (x === '0') { o = w.length > 0 ? w.pop() + o : `0${o}`; d++; b--; } else if (w.length > 0) { o = w.pop() + o; d++; b--; } while (b <= 0 && w.length > 0) { if (fmt.group && d === 3) { o = `,${o}`; d = 0; } o = w.pop() + o; d++; b--; } } else if (x === 'e' || x === 'E') { o = `${x}-${padNumber(Math.abs(exp), fmt.exponent)}${o}`; } else { o = x.slice(1) + o; } } if (fmt.aMask.length > 0) { let a = empty; let digits = false; for (const x of fmt.aMask) { switch (x) { case '0': { a = a + f.shift(); digits = true; break; } case '#': { if (f.reduce((acc, val) => { return val === '0' ? acc : true; }, false)) { a = a + f.shift(); digits = true; } break; } case 'e': case 'E': { a = a + x + ((fmt.signExponent || exp < 0) ? (exp < 0 ? '-' : '+') : empty) + padNumber(Math.abs(exp), fmt.exponent); break; } default: { a = a + x.slice(1); } } } o += digits ? `.${a}` : a; } return o; } //#endregion export default formatNumber;