"use strict"; /* TODO: toDisplay conversion between unit scales (eg. IEC -> metric bytes) ensure NaN is handled correctly Track the originally-constructed value internally, so that stacked conversions can be done losslessly? Additionally perhaps an isExact method that returns whether the current representation was the original one? */ const util = require("util"); const chalk = require("chalk"); const { validateArguments } = require("@validatem/core"); const required = require("@validatem/required"); const arrayOf = require("@validatem/array-of"); const isString = require("@validatem/is-string"); const isNumber = require("@validatem/is-number"); const dynamic = require("@validatem/dynamic"); const anything = require("@validatem/anything"); const allowExtraProperties = require("@validatem/allow-extra-properties"); function capitalize(string) { return string[0].toUpperCase() + string.slice(1); } module.exports = function makeUnits(_unitSpecs) { let [ unitSpecs ] = validateArguments(arguments, { unitSpecs: [ arrayOf([ { unit: [ required, isString ], toNext: [ isNumber ] }, dynamic((_value, { arrayIndex, arrayLength }) => { // FIXME: Actually test this let isLast = (arrayIndex === arrayLength - 1); if (isLast) { return anything; } else { return allowExtraProperties({ toNext: [ required ] }); } }) ]), ] }); let resultObject = {}; unitSpecs.forEach((spec, i) => { let proto = { [util.inspect.custom]: function (_depth, options) { let inspectString = ` ${this.amount} ${this.unit}`; if (options.colors === true) { return chalk.cyan(inspectString); } else { return inspectString; } }, toString: function () { return `${this.amount} ${this.unit}`; }, toDisplay: function (decimals) { let roundingFactor = Math.pow(10, decimals); let unitMagnitude = i; let amount = this.amount; let unitsLeft = (i < unitSpecs.length - 1); function createOfCurrentMagnitude() { let roundedValue = Math.round(amount * roundingFactor) / roundingFactor; return resultObject[unitSpecs[unitMagnitude].unit](roundedValue); } while (unitsLeft === true) { let currentUnit = unitSpecs[unitMagnitude]; if (amount < currentUnit.toNext) { return createOfCurrentMagnitude(); } else { amount = amount / currentUnit.toNext; unitMagnitude += 1; unitsLeft = (unitMagnitude < unitSpecs.length - 1); } } return createOfCurrentMagnitude(); } }; unitSpecs.forEach((otherSpec, otherI) => { let factor = 1; if (otherI < i) { /* Convert downwards, to smaller units (== larger numbers) */ unitSpecs.slice(otherI, i).reverse().forEach((specStep) => { factor = factor * specStep.toNext; }); } else if (otherI > i) { /* Convert upwards, to larger units (== smaller numbers) */ unitSpecs.slice(i, otherI).forEach((specStep) => { factor = factor / specStep.toNext; }); } proto[`to${capitalize(otherSpec.unit)}`] = function () { return resultObject[otherSpec.unit](this.amount * factor); }; }); resultObject[spec.unit] = function createUnit(value) { if (typeof value !== "number") { throw new Error("Value must be numeric"); } else { return Object.assign(Object.create(proto), { unit: spec.unit, amount: value }); } }; }); return resultObject; };