You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

128 lines
3.4 KiB
JavaScript

"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 = `<Unit> ${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;
};