STARK-web/static/js/wNumb.js
2023-10-04 17:24:40 +02:00

382 lines
9.2 KiB
JavaScript

(function(factory) {
if (typeof define === "function" && define.amd) {
// AMD. Register as an anonymous module.
define([], factory);
} else if (typeof exports === "object") {
// Node/CommonJS
module.exports = factory();
} else {
// Browser globals
window.wNumb = factory();
}
})(function() {
"use strict";
var FormatOptions = [
"decimals",
"thousand",
"mark",
"prefix",
"suffix",
"encoder",
"decoder",
"negativeBefore",
"negative",
"edit",
"undo"
];
// General
// Reverse a string
function strReverse(a) {
return a
.split("")
.reverse()
.join("");
}
// Check if a string starts with a specified prefix.
function strStartsWith(input, match) {
return input.substring(0, match.length) === match;
}
// Check is a string ends in a specified suffix.
function strEndsWith(input, match) {
return input.slice(-1 * match.length) === match;
}
// Throw an error if formatting options are incompatible.
function throwEqualError(F, a, b) {
if ((F[a] || F[b]) && F[a] === F[b]) {
throw new Error(a);
}
}
// Check if a number is finite and not NaN
function isValidNumber(input) {
return typeof input === "number" && isFinite(input);
}
// Provide rounding-accurate toFixed method.
// Borrowed: http://stackoverflow.com/a/21323330/775265
function toFixed(value, exp) {
value = value.toString().split("e");
value = Math.round(+(value[0] + "e" + (value[1] ? +value[1] + exp : exp)));
value = value.toString().split("e");
return (+(value[0] + "e" + (value[1] ? +value[1] - exp : -exp))).toFixed(exp);
}
// Formatting
// Accept a number as input, output formatted string.
function formatTo(
decimals,
thousand,
mark,
prefix,
suffix,
encoder,
decoder,
negativeBefore,
negative,
edit,
undo,
input
) {
var originalInput = input,
inputIsNegative,
inputPieces,
inputBase,
inputDecimals = "",
output = "";
// Apply user encoder to the input.
// Expected outcome: number.
if (encoder) {
input = encoder(input);
}
// Stop if no valid number was provided, the number is infinite or NaN.
if (!isValidNumber(input)) {
return false;
}
// Rounding away decimals might cause a value of -0
// when using very small ranges. Remove those cases.
if (decimals !== false && parseFloat(input.toFixed(decimals)) === 0) {
input = 0;
}
// Formatting is done on absolute numbers,
// decorated by an optional negative symbol.
if (input < 0) {
inputIsNegative = true;
input = Math.abs(input);
}
// Reduce the number of decimals to the specified option.
if (decimals !== false) {
input = toFixed(input, decimals);
}
// Transform the number into a string, so it can be split.
input = input.toString();
// Break the number on the decimal separator.
if (input.indexOf(".") !== -1) {
inputPieces = input.split(".");
inputBase = inputPieces[0];
if (mark) {
inputDecimals = mark + inputPieces[1];
}
} else {
// If it isn't split, the entire number will do.
inputBase = input;
}
// Group numbers in sets of three.
if (thousand) {
inputBase = strReverse(inputBase).match(/.{1,3}/g);
inputBase = strReverse(inputBase.join(strReverse(thousand)));
}
// If the number is negative, prefix with negation symbol.
if (inputIsNegative && negativeBefore) {
output += negativeBefore;
}
// Prefix the number
if (prefix) {
output += prefix;
}
// Normal negative option comes after the prefix. Defaults to '-'.
if (inputIsNegative && negative) {
output += negative;
}
// Append the actual number.
output += inputBase;
output += inputDecimals;
// Apply the suffix.
if (suffix) {
output += suffix;
}
// Run the output through a user-specified post-formatter.
if (edit) {
output = edit(output, originalInput);
}
// All done.
return output;
}
// Accept a sting as input, output decoded number.
function formatFrom(
decimals,
thousand,
mark,
prefix,
suffix,
encoder,
decoder,
negativeBefore,
negative,
edit,
undo,
input
) {
var originalInput = input,
inputIsNegative,
output = "";
// User defined pre-decoder. Result must be a non empty string.
if (undo) {
input = undo(input);
}
// Test the input. Can't be empty.
if (!input || typeof input !== "string") {
return false;
}
// If the string starts with the negativeBefore value: remove it.
// Remember is was there, the number is negative.
if (negativeBefore && strStartsWith(input, negativeBefore)) {
input = input.replace(negativeBefore, "");
inputIsNegative = true;
}
// Repeat the same procedure for the prefix.
if (prefix && strStartsWith(input, prefix)) {
input = input.replace(prefix, "");
}
// And again for negative.
if (negative && strStartsWith(input, negative)) {
input = input.replace(negative, "");
inputIsNegative = true;
}
// Remove the suffix.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice
if (suffix && strEndsWith(input, suffix)) {
input = input.slice(0, -1 * suffix.length);
}
// Remove the thousand grouping.
if (thousand) {
input = input.split(thousand).join("");
}
// Set the decimal separator back to period.
if (mark) {
input = input.replace(mark, ".");
}
// Prepend the negative symbol.
if (inputIsNegative) {
output += "-";
}
// Add the number
output += input;
// Trim all non-numeric characters (allow '.' and '-');
output = output.replace(/[^0-9\.\-.]/g, "");
// The value contains no parse-able number.
if (output === "") {
return false;
}
// Covert to number.
output = Number(output);
// Run the user-specified post-decoder.
if (decoder) {
output = decoder(output);
}
// Check is the output is valid, otherwise: return false.
if (!isValidNumber(output)) {
return false;
}
return output;
}
// Framework
// Validate formatting options
function validate(inputOptions) {
var i,
optionName,
optionValue,
filteredOptions = {};
if (inputOptions["suffix"] === undefined) {
inputOptions["suffix"] = inputOptions["postfix"];
}
for (i = 0; i < FormatOptions.length; i += 1) {
optionName = FormatOptions[i];
optionValue = inputOptions[optionName];
if (optionValue === undefined) {
// Only default if negativeBefore isn't set.
if (optionName === "negative" && !filteredOptions.negativeBefore) {
filteredOptions[optionName] = "-";
// Don't set a default for mark when 'thousand' is set.
} else if (optionName === "mark" && filteredOptions.thousand !== ".") {
filteredOptions[optionName] = ".";
} else {
filteredOptions[optionName] = false;
}
// Floating points in JS are stable up to 7 decimals.
} else if (optionName === "decimals") {
if (optionValue >= 0 && optionValue < 8) {
filteredOptions[optionName] = optionValue;
} else {
throw new Error(optionName);
}
// These options, when provided, must be functions.
} else if (
optionName === "encoder" ||
optionName === "decoder" ||
optionName === "edit" ||
optionName === "undo"
) {
if (typeof optionValue === "function") {
filteredOptions[optionName] = optionValue;
} else {
throw new Error(optionName);
}
// Other options are strings.
} else {
if (typeof optionValue === "string") {
filteredOptions[optionName] = optionValue;
} else {
throw new Error(optionName);
}
}
}
// Some values can't be extracted from a
// string if certain combinations are present.
throwEqualError(filteredOptions, "mark", "thousand");
throwEqualError(filteredOptions, "prefix", "negative");
throwEqualError(filteredOptions, "prefix", "negativeBefore");
return filteredOptions;
}
// Pass all options as function arguments
function passAll(options, method, input) {
var i,
args = [];
// Add all options in order of FormatOptions
for (i = 0; i < FormatOptions.length; i += 1) {
args.push(options[FormatOptions[i]]);
}
// Append the input, then call the method, presenting all
// options as arguments.
args.push(input);
return method.apply("", args);
}
function wNumb(options) {
if (!(this instanceof wNumb)) {
return new wNumb(options);
}
if (typeof options !== "object") {
return;
}
options = validate(options);
// Call 'formatTo' with proper arguments.
this.to = function(input) {
return passAll(options, formatTo, input);
};
// Call 'formatFrom' with proper arguments.
this.from = function(input) {
return passAll(options, formatFrom, input);
};
}
return wNumb;
});