You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
380 lines
9.2 KiB
380 lines
9.2 KiB
(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; |
|
});
|
|
|