@fto-consult/common
Version:
Un ensemble de bibliothèques et d'utilistaires communs pour le développement d'applications javascript
463 lines (408 loc) • 17 kB
JavaScript
import currencies from "./currencies";
import isString from "$cutils/isString";
import defaultStr from "$cutils/defaultStr";
import isNonNullString from "$cutils/isNonNullString";
const isObj = x => x && typeof x =='object';
import { getCurrency } from "$capp/currency";
import extendObj from "$cutils/extendObj";
const defaultObj = function() {
var args = Array.prototype.slice.call(arguments,0);
if(args.length === 1 && isObj(args[0])) return args[0];
for(var i in args){
let x = args[i];
if(isObj(x) && Object.size(x,true)>0) return x;
}
return {};
};
export * from "./utils";
// Create the local library object, to be exported or referenced globally later
const lib = {};
/* --- Exposed settings --- */
// The library's settings configuration object. Contains default parameters for
// currency and number formatting
lib.settings = {
get currency(){
return {
symbol : "FCFA", // default currency symbol is '$'
format : "%v %s", // controls output: %s = symbol, %v = value (can be object, see docs)
decimal : ".", // decimal point separator
thousand : " ", // thousands separator
decimal_digits : 0, // decimal places
grouping : 3, // digit grouping (not implemented yet)
...getCurrency()
}
},
number: {
decimal_digits : 0, // default decimal_digits on numbers is 0
grouping : 3, // digit grouping (not implemented yet)
thousand : " ",
decimal : "."
}
};
/* --- Internal Helper Methods --- */
/**
* Extends settings object
*
* Used for abstracting parameter handling from API methods
*/
function prepareOptions(...rest) {
const object = extendObj({},lib.settings.currency,...rest);
if(isNonNullString(object.format)){
const p = parseFormat(object.format);
if(p.format){
object.format = p.format;
}
if(typeof p.decimal_digits =="number"){
object.decimal_digits = p.decimal_digits;
}
}
return object;
}
/**
* Check and normalise the value of decimal_digits (must be positive integer)
*/
function checkPrecision(val, base) {
val = Math.round(Math.abs(val));
return isNaN(val)? base : val;
}
/**
* Parses a format string or object and returns format obj for use in rendering
*
* `format` is either a string with the default (positive) format, or object
* containing `pos` (required), `neg` and `zero` values (or a function returning
* either a string or object)
*
* Either string or format.pos must contain "%v" (value) to be valid
*/
function checkCurrencyFormat(format) {
let formatS = lib.settings.currency.format;
// Allow function as format parameter (should return string or object):
if ( typeof format === "function" ) format = format();
if(typeof format ==="string") format = format.toLowerCase();
// Format can be a string, in which case `value` ("%v") must be present:
if ( isString( format ) && format.match("%v") ) {
// Create and return positive, negative and zero formats:
return {
pos : format,
neg : format.replace("-", "").replace("%v", "-%v"),
zero : format
};
// If no format, or object is missing valid positive value, use formatS:
} else if ( !format || !format.pos || !format.pos.match("%v") ) {
// If formatS is a string, casts it to an object for faster checking next time:
return ( !isString( formatS ) ) ? formatS : lib.settings.currency.format = {
pos : formatS,
neg : formatS.replace("%v", "-%v"),
zero : formatS
};
}
// Otherwise, assume format was fine:
return format;
}
/* --- API Methods --- */
/**
* Takes a string/array of strings, removes all formatting/cruft and returns the raw float value
* Alias: `accounting.parse(string)`
*
* Decimal must be included in the regular expression to match floats (default options to
* accounting.settings.number.decimal), so if the number uses a non-standard decimal
* separator, provide it as the second argument.
*
* Also matches bracketed negatives (eg. "$ (1.99)" => -1.99)
*
* Doesn't throw any errors (`NaN`s become 0) but this may change in future
*/
export const unformat = lib.unformat = lib.parse = function(value, decimal) {
// Recursively unformat arrays:
if ((value) && typeof value === 'object') {
return Object.mapToArray(value,function(val) {
return unformat(val, decimal);
});
}
// Fails silently (need decent errors):
value = value || 0;
// Return the value as-is if it's already a number:
if (typeof value === "number") return value;
// Default decimal point comes from settings, but could be set to eg. "," in opts:
decimal = decimal || lib.settings.number.decimal;
// Build regex to strip out everything except digits, decimal point and minus sign:
var regex = new RegExp("[^0-9-" + decimal + "]", ["g"]),
unformatted = parseFloat(
("" + value)
.replace(/\((?=\d+)(.*)\)/, "-$1") // replace bracketed values with negatives
.replace(regex, '') // strip out any cruft
.replace(decimal, '.') // make sure decimal point is standard
);
// This will fail silently which may cause trouble, let's wait and see:
return !isNaN(unformatted) ? unformatted : 0;
};
export const parse = unformat;
/**
* Implementation of toFixed() that treats floats more like decimals
*
* Fixes binary rounding issues (eg. (0.615).toFixed(2) === "0.61") that present
* problems for accounting- and finance-related software.
*/
export const toFixed = lib.toFixed = function(value, decimal_digits) {
decimal_digits = checkPrecision(decimal_digits, lib.settings.number.decimal_digits);
var exponentialForm = Number(lib.unformat(value) + 'e' + decimal_digits);
var rounded = Math.round(exponentialForm);
var finalResult = Number(rounded + 'e-' + decimal_digits).toFixed(decimal_digits);
return finalResult;
};
/**
* Format a number, with comma-separated thousands and custom decimal_digits/decimal places
* Alias: `accounting.format()`
*
* Localise by overriding the decimal_digits and thousand / decimal separators
* 2nd parameter `decimal_digits` can be an object matching `settings.number`
*/
export const formatNumber = lib.formatNumber = lib.format = function(number, decimal_digits, thousand, decimal) {
// Resursively format arrays:
if ((number) && typeof number === 'object') {
return Object.mapToArray(number, function(val) {
return formatNumber(val, decimal_digits, thousand, decimal);
});
}
// Clean up number:
number = unformat(number);
// Build options object from second param (if object) or all params, extending default options :
var opts = prepareOptions(
(isObj(decimal_digits) ? decimal_digits : {
decimal_digits : decimal_digits,
thousand : thousand,
decimal : decimal
}),
),
// Clean up decimal_digits
usePrecision = checkPrecision(opts.decimal_digits),
// Do some calc:
negative = number < 0 ? "-" : "",
base = parseInt(toFixed(Math.abs(number || 0), usePrecision), 10) + "",
mod = base.length > 3 ? base.length % 3 : 0;
let decimalStr = "";
if(usePrecision){
const fNum = String(parseFloat(toFixed(Math.abs(number), usePrecision)) || 0);
if(fNum.includes(".")){
decimalStr = defaultStr(fNum.split(".")[1]).trim();
}
}
// Format the number:
return negative + (mod ? base.substr(0, mod) + opts.thousand : "") + base.substr(mod).replace(/(\d{3})(?=\d)/g, "$1" + opts.thousand) + (usePrecision && decimalStr ? (opts.decimal + decimalStr) : "");
};
/*** use for digit grouping
*
*
function commafy( number,separator,decimal ) {
if(isNonNullString(separator)){
separator = " ";
}
if(typeof decimal != 'string') decimal = prepareOptions({},lib.settings.currency).decimal
number += ''; // stringify
var num = number.split(decimal); // incase decimals
if (typeof num[0] !== 'undefined'){
var int = num[0]; // integer part
if (int.length > 4){
int = int.split('').reverse().join(''); // reverse
int = int.replace(/(\d{3})/g, "$1,"); // add commas
int = int.split('').reverse().join(''); // unreverse
}
}
if (typeof num[1] !== 'undefined'){
var dec = num[1]; // float part
if (dec.length > 4){
dec = dec.replace(/(\d{3})/g, "$1"+separator); // add spaces
}
}
return (typeof num[0] !== 'undefined'?int:'')
+ (typeof num[1] !== 'undefined'?'.'+dec:'');
}*/
/**
* Format a number into currency
*
le symbole peut être un objet, dans ce cas les autres propriétés peuvent être nulles
* Usage: accounting.formatMoney(number, symbol, decimal_digits, thousandsSep, decimalSep, format)
* prepareOptions: (0, "$", 2, ",", ".", "%s%v")
*
* Localise by overriding the symbol, decimal_digits, thousand / decimal separators and format
* Second param can be an object matching `settings.currency` which is the easiest way.
*
* To do: tidy up the parameters
*/
export const formatMoney = lib.formatMoney = function(number, symbol, decimal_digits, thousand, decimal, format,returnObject) {
// Resursively format arrays:
if ((number) && typeof number === 'object') {
return Object.mapToArray(number, function(val){
return formatMoney(val, symbol, decimal_digits, thousand, decimal, format,returnObject);
});
}
// Clean up number:
number = unformat(number);
// Build options object from second param (if object) or all params, extending default options :
const opts = prepareOptions(
(isObj(symbol) ? symbol: {
symbol,
decimal_digits,
thousand,
decimal,
format
})),
// Check format (returns object with pos, neg and zero):
formats = defaultObj(checkCurrencyFormat(opts.format)),
// Choose which format to use for this value:
useFormat = defaultStr(number > 0 ? formats.pos : number < 0 ? formats.neg : formats.zero);
const formattedValue = useFormat.replace('%s', opts.symbol);
const formattedNumber = formatNumber(Math.abs(number), checkPrecision(opts.decimal_digits), opts.thousand, opts.decimal);
const formattedResult = formattedValue.replace('%v',formattedNumber);
if(returnObject ===true){
return {
...opts,
formattedValue,
formattedNumber,
symbol : opts.symbol,
useFormat,
formattedResult,
}
}
// Return with currency symbol added:
return formattedResult;
};
/**
* Format a list of numbers into an accounting column, padding with whitespace
* to line up currency symbols, thousand separators and decimals places
*
* List should be an array of numbers
* Second parameter can be an object containing keys that match the params
*
* Returns array of accouting-formatted number strings of same length
*
* NB: `white-space:pre` CSS rule is required on the list container to prevent
* browsers from collapsing the whitespace in the output strings.
*/
lib.formatColumn = function(list, symbol, decimal_digits, thousand, decimal, format) {
if (!list || !isArray(list)) return [];
var opts = prepareOptions(
(isObj(symbol) ? symbol : {
symbol : symbol,
decimal_digits : decimal_digits,
thousand : thousand,
decimal : decimal,
format : format
})
),
// Check format (returns object with pos, neg and zero), only need pos for now:
formats = checkCurrencyFormat(opts.format),
// Whether to pad at start of string or after currency symbol:
padAfterSymbol = formats.pos.indexOf("%s") < formats.pos.indexOf("%v") ? true : false,
// Store value for the length of the longest string in the column:
maxLength = 0,
// Format the list according to options, store the length of the longest string:
formatted = Object.mapToArray(list, function(val, i) {
if (isArray(val)) {
// Recursively format columns if list is a multi-dimensional array:
return lib.formatColumn(val, opts);
} else {
// Clean up the value
val = unformat(val);
// Choose which format to use for this value (pos, neg or zero):
var useFormat = val > 0 ? formats.pos : val < 0 ? formats.neg : formats.zero,
// Format this value, push into formatted list and save the length:
fVal = useFormat.replace('%s', opts.symbol).replace('%v', formatNumber(Math.abs(val), checkPrecision(opts.decimal_digits), opts.thousand, opts.decimal));
if (fVal.length > maxLength) maxLength = fVal.length;
return fVal;
}
});
// Pad each number in the list and send back the column of numbers:
return Object.mapToArray(formatted, function(val, i) {
// Only if this is a string (not a nested array, which would have already been padded):
if (isString(val) && val.length < maxLength) {
// Depending on symbol position, pad after symbol or at index 0:
return padAfterSymbol ? val.replace(opts.symbol, opts.symbol+(new Array(maxLength - val.length + 1).join(" "))) : (new Array(maxLength - val.length + 1).join(" ")) + val;
}
return val;
});
};
export const getDefaultCurrency = ()=>{
let currency = {};
if(lib.settings.currency){
const symbol = defaultStr(lib.settings.currency.symbol);
if(symbol){
for(let i in currencies){
currency = currencies[i];
if(!isObj(currency)) continue;
if(currency.symbol == symbol && isNonNullString(currency.name_plural)){
return currency;
}
}
}
}
return defaultObj(lib.settings.currency);
};
/***
parse le format de la devise, et retourne un objet portant le cas échéants 2 propriétés :
format : le format de la dévise parsé
decimals : le nombre de décimales le cas où il est inclut dans le format
@param {string} format, le format de la dévise, chaine de caractère combinant les caractères %s, %v et .#{0,n}, où :
- %s représente le code de la dévise d'affichage,
- %v représente la valeur du nombre à formatté
- .#{0,n} représeente le nombre de décimales que l'on souhaite utiliser pour le format. avec n le n'ombre de décimales
Par exemple, le format %v %s .### permet de formatter le nombre 12555.6893300244 en $ de la sorte : [12555.689 $], avec 3 décimales
Si l'on souhaite n'avoir pas de décimales dans le format, il suffit de ne préciser aucun # après le point (.); par example : %s %v .
*/
export const parseFormat = lib.parseFormat = (format)=>{
format = defaultStr(format).trim();
const ret = {};
if(format){
const reg = /(\.)(\#{0,9}\s*$)/; //le format contient le nombre de décimales spécifiés.
const m = format.match(reg);
if(Array.isArray(m)){
if(m.length === 3){
//on récupère le nombre de décimales
ret.decimal_digits = defaultStr(m[2]).trim().length; //le nombre de décimales est récupéré
}
format = format.replace(reg,""); //on remplace l'expression regulière trouvée dans le format
}
//si le format inclut le motif .[d], avec d un nombre alors le nombre suivant represente le nombre de décimale
ret.format = format;
}
return ret;
}
export const formatDescription = `Format d'affichage des valeurs numériques : une chaine de caractère constituée des lettre %v et %s où %v représente la valeur du montant et %s représente la devise : exemple %s%v => $10 et %v %s => 10 $.
Pour définir les décimales, utiliser le motif [.][#{0,n}], exemple : .### pour l'affichage avec 3 decimales et . pour ne pas inclure les decimales dans l'affichage.
par exemple, Le format %v %s .## retourne : 12.35 $ pour la valeur 12.357777 convertie en dollard.
`;
const CurrenCy = {
updateCurrency : (currency,format,decimal_digits)=>{
const options = typeof currency =="object" && currency ? currency : {};
if(isNonNullString(options.currency) && currencies[options.currency]){
currency = options.currency;
}
if(isNonNullString(currency) && currencies[currency]){
currency = currencies[currency];
}
lib.settings.currency = {
...lib.settings.currency,
...defaultObj(currency)
}
decimal_digits = typeof decimal_digits =='number'? decimal_digits : typeof options.currencyDecimalDigits =='number'? options.currencyDecimalDigits : undefined;
const p = parseFormat(defaultStr(format,options.currencyFormat).trim())
if(p.format){
lib.settings.currency.format = p.format;
}
if(typeof p.decimal_digits =="number"){
decimal_digits = p.decimal_digits;
}
if(typeof decimal_digits == "number"){
lib.settings.number.decimal_digits =lib.settings.currency.decimal_digits = decimal_digits;
}
return lib.settings;
},
/**** retourne l'objet currency actuel */
getDefaultCurrency,
currencies,
format : lib.formatMoney,
...lib,
formatDescription,
}
export default CurrenCy;
export {currencies};