/**
* A collection of functions and classes for dealing with `number unit`
* expressions. These expressions can be converted to other units, added,
* subtracted, transformed to a more user friendly representation, and used to
* generate conversions for all units in the same "class".
*
* @namespace
*/
var Unitz = {};
/**
* A value which can converted to a {@link Unitz.Parsed} instance.
*
* - `'1.5'`: A unitless value.
* - `'1/2'`: A unitless fraction.
* - `'2 1/3'`: A unitless fraction with a whole number.
* - `'unit'`: A unit with a value of 1.
* - `'0.5 unit'`: A number value with a unit.
* - `'1/2 unit'`: A fraction with a unit.
* - `'3 1/4 unit'`: A fraction with a whole number with a unit.
* - `1.5`: A unitless value.
* - `Unitz.Parsed instance`
*
* @typedef {String|Object|Number} parsable
*/
/**
* An array of all unit classes.
*
* @memberof Unitz
* @type {Unitz.Class[]}
* @see Unitz.addClass
*/
var classes = [];
/**
* A map of all unit classes where the key is their {@link Unitz.Class#className}.
*
* @memberof Unitz
* @type {Object}
* @see Unitz.addClass
*/
var classMap = {};
/**
* A map of all units (lowercased) to their {@link Unitz.Class}
*
* @memberof Unitz
* @type {Object}
* @see Unitz.addClass
*/
var unitToClass = {};
function isString(x)
{
return typeof x === 'string';
}
function isObject(x)
{
return x !== null && typeof x === 'object';
}
function isNumber(x)
{
return typeof x === 'number' && !isNaN(x);
}
function isArray(x)
{
return x instanceof Array;
}
/**
* Removes a unit from its class. The group the unit to still exists in the
* class, but the unit won't be parsed to the group anymore.
*
* @method
* @memberof Unitz
* @param {String} unit -
* The lowercase unit to remove from this class.
* @return {Boolean} -
* True if the unit was removed, false if it does not exist in this class.
*/
function removeUnit(unit)
{
var removed = false;
if ( unit in unitToClass )
{
removed = unitToClass[ unit ].removeUnit( unit );
}
return removed;
}
/**
* Removes the group which has the given unit. The group will be removed
* entirely from the system and can no longer be parsed or converted to and
* from.
*
* @method
* @memberof Unitz
* @param {String} unit -
* The lowercase unit of the group to remove.
* @return {Boolean} -
* True if the group was removed, false if it does not exist in this class.
*/
function removeGroup(unit)
{
var removed = false;
if ( unit in unitToClass )
{
removed = unitToClass[ unit ].removeGroup( unit );
}
return removed;
}
/**
* Determines if the given variable is a number equivalent to one (both positive
* and negative). This is used to determine when to use the singluar or plural
* version of a unit.
*
* @memberof Unitz
* @param {Number} x -
* The number to check for oneness.
* @return {Boolean} -
* True if the given number is equivalent to +1 or -1.
*/
function isSingular(x)
{
return isNumber( x ) && Math.abs( Math.abs( x ) - 1 ) < Unitz.epsilon;
}
/**
* Determines if the given variable is a whole number.
*
* @memberof Unitz
* @param {Number} x -
* The number to check for wholeness.
* @return {Boolean} -
* True if the given number is a whole number, otherwise false.
*/
function isWhole(x)
{
return isNumber( x ) && Math.abs( Math.floor( x ) - x ) < 0.00000001;
}
/**
* Determines whether the two units are close enough a match to be considered
* units of the same group. This is used when units are given by the user which
* aren't known to Unitz. It determines this by comparing the first
* {@link Unitz.heuristicLength} characters of each unit.
*
* ```javascript
* Unitz.isHeuristicMatch('loaves', 'loaf'); // true
* Unitz.isHeuristicMatch('taco', 'tacos'); // true
* Unitz.isHeuristicMatch('pk', 'pack'); // false
* ```
*
* @memberof Unitz
* @param {String} unitA -
* The first unit to test.
* @param {String} unitB -
* The second string to test.
* @return {Boolean} -
* True if the two units are a match, otherwise false.
*/
function isHeuristicMatch(unitA, unitB)
{
return unitA.substring( 0, Unitz.heuristicLength ) === unitB.substring( 0, Unitz.heuristicLength );
}
/**
* Creates a stirng representation of a value and a unit. If the value doesn't
* have a unit then the value is returned immediately.
*
* @memberof Unitz
* @param {Number|String} value -
* The value to add a unit to.
* @param {String} [unit] -
* The unit to add to the value.
* @return {String} -
* The normal representation of a value and its unit.
*/
function createNormal(value, unit)
{
return unit ? value + ' ' + unit : value;
}
/**
* Converts input to an array of
*
* @memberof Unitz
* @param {String|Array|Object} [input] -
* The input to convert to an array.
* @return {Array} -
* The array of converted inputs.
* @see Unitz.combine
* @see Unitz.subtract
*/
function splitInput(input)
{
if ( isString( input ) )
{
return input.split( Unitz.separator );
}
if ( isArray( input ) )
{
return input;
}
if ( isObject( input ) || isNumber( input ) )
{
return [ input ];
}
return [];
}
/**
* Parses the input and returns an instance of {@link Unitz.Parsed}. If the
* given input cannot be parsed then `false` is returned.
*
* @memberof Unitz
* @param {parsable} input -
* The parsable input.
* @return {Unitz.Parsed} -
* The parsed instance.
*/
function parseInput(input)
{
if ( isString( input ) )
{
return parse( input );
}
if ( isObject( input ) )
{
return input;
}
if ( isNumber( input ) )
{
return UnitzParsed.fromNumber( input );
}
return false;
}
/**
* Parses a number and unit out of the given string and returns a parsed
* instance. If the given input is not in a valid format `false` is returned.
*
* ```javascript
* Unitz.parse('1.5'); // A unitless value.
* Unitz.parse('1/2'); // A unitless fraction.
* Unitz.parse('2 1/3'); // A unitless fraction with a whole number.
* Unitz.parse('unit'); // A unit with a value of 1.
* Unitz.parse('0.5 unit'); // A number value with a unit.
* Unitz.parse('1/2 unit'); // A fraction with a unit.
* Unitz.parse('3 1/4 unit'); // A fraction with a whole number with a unit.
* Unitz.parse(''); // false
* ```
*
* @memberof Unitz
* @param {String} input -
* The input to parse a number & unit from.
* @return {Unitz.Parsed} -
* The parsed instance.
*/
function parse(input)
{
var group = Unitz.regex.exec( input );
var whole = group[1];
var numer = group[3];
var denom = group[5];
var decimal = group[6];
var unit = group[7].toLowerCase();
if ( !whole && !decimal && !unit )
{
return false;
}
var value = 1;
if ( whole )
{
value = parseInt( whole );
var sign = (value < 0 ? -1 : 1);
if ( denom )
{
denom = parseInt( denom );
if ( numer )
{
value += ( parseInt( numer ) / denom ) * sign;
}
else
{
value /= denom;
}
}
else if ( decimal )
{
value += parseFloat( '0.' + decimal ) * sign;
}
}
return new UnitzParsed( value, unit, unitToClass[ unit ], input );
}
/**
* Parses a number and unit out of the given string and returns a human friendly
* representation of the number and unit class - which is known as a compound
* representation because it can contain as many units as necessary to
* accurately describe the value. This is especially useful when you want
* precise amounts for a fractional value.
*
* ```javascript
* Unitz.compound('2 cups', ['pt', 'c']); // '1 pt'
* Unitz.compound('2 cups', ['c', 'tbsp']); // '2 c'
* Unitz.compound('0.625 cups', ['c', 'tbsp', 'tsp']); // '1/2 c, 2 tbsp'
* Unitz.compound('1.342 cups', ['c', 'tbsp', 'tsp']); // '1 c, 5 tbsp, 1 tsp'
* ```
*
* @memberof Unitz
* @param {String} input -
* The input to parse a number & unit from.
* @param {String[]} [unitsAllowed=false] -
* The units to be restricted to use. This can be used to avoid using
* undesirable units in the output. If this is not given, then all units for
* the parsed input may be used.
* @return {String} -
* The compound string built from the input.
*/
function compound(input, unitsAllowed)
{
var parsed = parseInput( input );
var compound = [];
if ( parsed.unitClass && parsed.group )
{
var groups = parsed.unitClass.groups;
for (var i = groups.length - 1; i >= 0; i--)
{
var grp = groups[ i ];
// If no specific units are desired OR the current group is a desired unit...
if ( !unitsAllowed || unitsAllowed.indexOf( grp.unit ) !== -1 )
{
var converted = parsed.convert( grp.unit );
var denoms = grp.denominators;
// Try out each denominator in the given group.
for (var k = 0; k < denoms.length; k++)
{
var den = denoms[ k ];
var num = Math.floor( den * converted );
// If the numerator to the current fraction is greater than zero then
// use this group as the next statement in the compound string.
if ( num >= 1 )
{
var actual = num / den;
var whole = Math.floor( actual );
var part = '';
if ( whole >= 1 )
{
part += whole;
num -= whole * den;
}
if ( num > 0 && den > 1 )
{
part += (part.length > 0 ? ' ' : '') + num + '/' + den;
}
part = createNormal( part, grp.unit );
compound.push( part );
parsed.value -= convert( part, parsed.unit );
break;
}
}
}
}
}
return compound.length ? compound.join( ', ' ) : parsed.normal;
}
/**
* Determines the best way to represent the parsable input, optionally using
* fractions. This will look at all units available in the parsed class and
* use the conversion which results in the closest representation with the
* shortest string representation favoring larger units. A {@link Unitz.Parsed}
* instance is returned with the {@link Unitz.Parsed#normal} property set to
* the best representation.
*
* ```javascript
* Unitz.best('2 pints'); // '1 quart'
* Unitz.best('2640 ft'); // '0.5 miles'
* Unitz.best('2640 ft', true); // '1/2 mile'
* ```
*
* @memberof Unitz
* @param {parsable} input -
* The input to return the best representation of.
* @param {Boolean} [returnFraction=false] -
* If the best representation should attempted to be a fraction.
* @param {Boolean} [abbreviations=false] -
* If the returned value should use abbreviations if they're available.
* @param {Number} [largestDenominator] -
* See {@link Unitz.Fraction}.
* @return {Unitz.Parsed} -
* The parsed instance with the {@link Unitz.Parsed#normal} property set to
* the best representation.
*/
function best(input, returnFraction, abbreviations, largestDenominator)
{
var parsed = parseInput( input );
if ( parsed.unitClass )
{
// out of all groups in class, calculate converted value fraction and
// take the one that is a whole number or is the closest to a whole
// number while being the closest
var closest = null;
var closestGroup = null;
var groups = parsed.unitClass.groups;
for (var i = 0; i < groups.length; i++)
{
var grp = groups[ i ];
var fraction = parsed.convert( grp.unit, true );
if ( fraction.valid )
{
var better = !closest;
if ( closest )
{
var closeApprox = fraction.distance <= closest.distance;
var closeString = fraction.string.length <= closest.string.length;
var closeActual = (fraction.actual + '').length <= (closest.actual + '').length;
if ( closeApprox && (returnFraction ? closeString : closeActual) )
{
better = true;
}
}
if ( better )
{
closest = fraction;
closestGroup = grp;
}
}
}
if ( closest )
{
if ( parsed === input )
{
parsed = new UnitzParsed( closest.actual, closestGroup.unit, parsed.unitClass );
}
else
{
parsed.value = closest.actual;
parsed.unit = closestGroup.unit;
parsed.group = closestGroup;
}
parsed.normal = returnFraction ?
createNormal( closest.string, closestGroup.getUnit( closest.isSingular(), abbreviations ) ) :
closestGroup.addUnit( closest.actual, abbreviations );
}
}
return parsed;
}
/**
* Converts the parsable input to the given unit. If the conversion can't be
* done then false is returned.
*
* ```javascript
* Unitz.convert('30 in', 'ft'); // 2.5
* Unitz.convert('1 in', 'cm'); // 2.54
* Unitz.convert('2 1/2 gal', 'qt'); // 10
* ```
*
* @memberof Unitz
* @param {parsable} input -
* The input to parse and convert to the given unit.
* @param {String} unit -
* The unit to convert to.
* @return {Number} -
* The converted number.
*/
function convert(input, unit)
{
var parsed = parseInput( input );
// Not valid input? return false
if ( !isObject( parsed ) )
{
return false;
}
var value = parsed.value;
var unitClass = parsed.unitClass;
// If there was no unit class and no unit provided, return the unitless value.
if ( !unitClass && !unit )
{
return value;
}
// If there was no unit class parsed OR the given unit is not in the same class then return false!
if ( !unitClass || !(unit in unitClass.converters) )
{
return false;
}
// If the parsed unit and requested unit is the same, return the parsed value.
if ( unitClass.groupMap[ unit ] === unitClass.groupMap[ parsed.unit ] )
{
return value;
}
// Convert the parsed value to its base unit
value *= unitClass.converters[ parsed.unit ];
// If they don't have the same bases convert the parsed value
var baseFrom = unitClass.bases[ parsed.unit ];
var baseTo = unitClass.bases[ unit ];
if ( baseFrom !== baseTo )
{
value *= unitClass.mapping[ baseFrom ][ baseTo ];
}
// Divide the value by the desired unit.
value /= unitClass.converters[ unit ];
return value;
}
/**
* Given an array of unknown units - return the singular or plural unit. The
* singular unit is the shorter string and the plural unit is the longer string.
*
* @memberof Unitz
* @param {String[]} units -
* The array of units to look through.
* @param {Boolean} singular -
* True if the singular unit should be returned, otherwise false if the
* plural unit should be returned.
* @return {String} -
* The singular or plural unit determined.
*/
function findUnit(units, singular)
{
var chosen = '';
for (var i = 0; i < units.length; i++)
{
var u = units[ i ];
if ( u.length && (chosen === '' || (singular && u.length < chosen.length) || (!singular && u.length > chosen.length) ) )
{
chosen = u;
}
}
return chosen;
}
/**
* Adds the two expressions together into a single string. Each expression can
* be a comma delimited string of value & unit pairs - this function will take
* the parsed values with the same classes and add them together. The string
* returned has expressions passed through the {@link Unitz.best} function
* optionally using fractions.
*
* ```javascript
* Unitz.combine( 2, '3 tacos' ); // '5 tacos'
* Unitz.combine( '2 cups', '1 pt' ); // '1 quart'
* Unitz.combine( '3 cups, 1 bag', '2 bags, 12 tacos' ); // `3 cups, 3 bags, 12 tacos'
* ```
*
* @memberof Unitz
* @param {String|parsable|parsable[]} inputA -
* The first expression or set of expressions to add together.
* @param {String|parsable|parsable[]} inputB -
* The second expression or set of expressions to add together.
* @param {Boolean} [fraction=false] -
* If the returned value should attempt to use fractions.
* @param {Boolean} [abbreviations=false] -
* If the returned value should use abbreviations if they're available.
* @param {Number} [largestDenominator] -
* See {@link Unitz.Fraction}.
* @return {String} -
* The string representation of `inputA + inputB`.
*/
function combine(inputA, inputB, fraction, abbreviations, largestDenominator)
{
var splitA = splitInput( inputA );
var splitB = splitInput( inputB );
var splitBoth = splitA.concat( splitB );
var parsed = [];
// Parse all inputs - ignore invalid inputs
for (var i = 0; i < splitBoth.length; i++)
{
var parsedInput = parseInput( splitBoth[ i ] );
if ( parsedInput !== false )
{
parsedInput.units = [];
parsedInput.units.push( parsedInput.unit );
parsed.push( parsedInput );
}
}
// Try merging subsequent (k) parsed values into this one (i)
for (var i = 0; i < parsed.length - 1; i++)
{
var a = parsed[ i ];
for (var k = parsed.length - 1; k > i; k--)
{
var b = parsed[ k ];
var converted = b.convert( a.unit );
// Same unit class. We can use proper singular/plural units.
if ( converted !== false && a.group )
{
parsed.splice( k, 1 );
a.value += converted;
}
// "a" or "b" doesn't have a unit
else if ( !a.unit || !b.unit )
{
parsed.splice( k, 1 );
a.value += b.value;
a.units = a.units.concat( b.units );
}
// "a" and "b" have a similar enough unit.
else if ( isHeuristicMatch( a.unit, b.unit ) )
{
parsed.splice( k, 1 );
a.value += b.value;
a.units = a.units.concat( b.units );
}
}
}
var combined = [];
for (var i = 0; i < parsed.length; i++)
{
var a = parsed[ i ];
if ( a.group )
{
a.normal = a.group.addUnit( a.value, abbreviations );
}
else
{
a.unit = findUnit( a.units, isSingular( a.value ) );
a.normal = createNormal( a.value, a.unit );
}
var parsedBest = best( a, fraction, abbreviations, largestDenominator );
if ( parsedBest && parsedBest.normal )
{
combined.push( parsedBest.normal );
}
}
return combined.join( Unitz.separatorJoin );
}
/**
* Subtracts the second expression from the first expression and returns a
* string representation of the results. Each expression can be a comma
* delimited string of value & unit pairs - this function will take
* the parsed values with the same classes and subtract them from each other.
* The string returned has expressions passed through the {@link Unitz.best}
* function optionally using fractions. By default negative quantities are not
* included in the result but can overriden with `allowNegatives`.
*
* ```javascript
* Unitz.subtract( '3 tacos', '1 taco' ); // '2 tacos'
* Unitz.subtract( 4, 1 ); // '3'
* Unitz.subtract( '3 cups, 1 bag', '2 bags, 12 tacos' ); // `3 cups'
* ```
*
* @memberof Unitz
* @param {String|parsable|parsable[]} inputA -
* The expression to subtract from.
* @param {String|parsable|parsable[]} inputB -
* The expression to subtract from `inputA`.
* @param {Boolean} [allowNegatives=false] -
* Whether or not negative values should be included in the results.
* @param {Boolean} [fraction=false] -
* If the returned value should attempt to use fractions.
* @param {Boolean} [abbreviations=false] -
* If the returned value should use abbreviations if they're available.
* @param {Number} [largestDenominator] -
* See {@link Unitz.Fraction}.
* @return {String} -
* The string representation of `inputA - inputB`.
*/
function subtract(inputA, inputB, allowNegatives, fraction, abbreviations, largestDenominator)
{
var splitA = splitInput( inputA );
var splitB = splitInput( inputB );
var splitBoth = splitA.concat( splitB );
var parsed = [];
// Parse all inputs - ignore invalid inputs
for (var i = 0; i < splitBoth.length; i++)
{
var parsedInput = parseInput( splitBoth[ i ] );
if ( parsedInput !== false )
{
parsedInput.sign = i >= splitA.length ? -1 : 1;
parsedInput.units = [];
parsedInput.units.push( parsedInput.unit );
parsed.push( parsedInput );
}
}
// Try merging subsequent (k) parsed values into this one (i)
for (var i = 0; i < parsed.length - 1; i++)
{
var a = parsed[ i ];
for (var k = parsed.length - 1; k > i; k--)
{
var b = parsed[ k ];
var converted = b.convert( a.unit );
var sign = b.sign * a.sign;
// Same unit class. We can use proper singular/plural units.
if ( converted !== false && a.group )
{
parsed.splice( k, 1 );
a.value += converted * sign;
}
// "a" or "b" doesn't have a unit
else if ( !a.unit || !b.unit )
{
parsed.splice( k, 1 );
a.value += b.value * sign;
a.units = a.units.concat( b.units );
}
// "a" and "b" have a similar enough unit.
else if ( isHeuristicMatch( a.unit, b.unit ) )
{
parsed.splice( k, 1 );
a.value += b.value * sign;
a.units = a.units.concat( b.units );
}
}
}
var combined = [];
for (var i = 0; i < parsed.length; i++)
{
var a = parsed[ i ];
if ( (a.value < 0 || a.sign < 0) && !allowNegatives )
{
continue;
}
if ( a.group )
{
a.normal = a.group.addUnit( a.value, abbreviations );
}
else
{
a.unit = findUnit( a.units, isSingular( a.value ) );
a.normal = createNormal( a.value, a.unit );
}
var parsedBest = best( a, fraction, abbreviations, largestDenominator );
if ( parsedBest && parsedBest.normal )
{
combined.push( parsedBest.normal );
}
}
return combined.join( Unitz.separatorJoin );
}
/**
* Parses the given input and returns a {@link Unitz.Parsed} instance with a new
* `conversions` property which is an array of {@link Unitz.Conversion}s.
* The array of conversions generated can be limited by minimum and maximum
* numbers to only return human friendly conversions.
*
* ```javascript
* Unitz.conversions('2.25 hrs', 0.1, 1000); // '135 minutes', '2 1/4 hours'
* ```
*
* @memberof Unitz
* @param {parsable} input -
* The input to generate conversions for.
* @param {Number} [min] -
* If given, the conversions returned will all have values above `min`.
* @param {Number} [max] -
* If given, the conversions returned will all have values below `max`.
* @param {Number} [largestDenominator] -
* See {@link Unitz.Fraction}.
* @return {Unitz.Parsed} -
* The instance parsed from the input with a `conversions` array. If the
* parsed input is not valid or has a unit class then the input given is
* returned.
*/
function conversions(input, min, max, largestDenominator)
{
var parsed = parseInput( input );
if ( !isObject( parsed ) || !parsed.unitClass )
{
return input;
}
var groups = parsed.unitClass.groups;
var conversions = parsed.conversions = [];
for (var i = 0; i < groups.length; i++)
{
var grp = groups[ i ];
var converted = parsed.convert( grp.unit );
if ( !isNumber( converted ) )
{
continue;
}
if ( isNumber( min ) && converted < min )
{
continue;
}
if ( isNumber( max ) && converted > max )
{
continue;
}
var fraction = new UnitzFraction( converted, grp.denominators, largestDenominator );
conversions.push(new UnitzConversion( converted, fraction, grp ));
}
return parsed;
}
/**
* Adds a new {@link Unitz.Class} to Unitz registering all converters in the
* class to be available for parsing.
*
* @memberof Unitz
* @param {Unitz.Class} unitClass -
* The unit class to add.
*/
function addClass(unitClass)
{
classMap[ unitClass.className ] = unitClass;
classes.push( unitClass );
for (var unit in unitClass.converters)
{
unitToClass[ unit ] = unitClass;
}
}