rafaels88/pinfluence

View on GitHub
apps/web/assets/javascripts/vendors/wNumb.js

Summary

Maintainability
C
1 day
Test Coverage
(function(){

    'use strict';

var
/** @const */ FormatOptions = [
    'decimals',
    'thousand',
    'mark',
    'prefix',
    'postfix',
    '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 postfix.
    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.
    function toFixed ( value, decimals ) {
        var scale = Math.pow(10, decimals);
        return ( Math.round(value * scale) / scale).toFixed( decimals );
    }


// Formatting

    // Accept a number as input, output formatted string.
    function formatTo ( decimals, thousand, mark, prefix, postfix, 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 postfix.
        if ( postfix ) {
            output += postfix;
        }

        // 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, postfix, 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 postfix.
        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice
        if ( postfix && strEndsWith(input, postfix) ) {
            input = input.slice(0, -1 * postfix.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 = {};

        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);
    }

    /** @constructor */
    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);
        };
    }

    /** @export */
    window.wNumb = wNumb;

}());