diff --git a/README.md b/README.md index e063e96..5a351d9 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,35 @@ const isCompatibleUnit = UnitsHelper.isCompatibleUnit('lbs', 'tons'); console.log(isCompatibleUnit); // true ``` -Other useful functions... +More commonly, you will use the line item/product functions or to generate units for select boxes ```js -UnitsHelper.isLiquidUnit('floz') // true -UnitsHelper.convertToGallons(8, 'pints') // 1 -UnitsHelper.convertToUnit(8, 'pints', 'gallons') // 1 -UnitsHelper.convertToUnit(8, 'lbs', 'gallons') // throw ConversionError +UnitsHelper.perAcreCost(product, item, acres) // 3.50 +UnitsHelper.listAvailableUnits(product) // ['gallons', 'floz', 'milliliters', ...] +``` + +All of this is build off of the `Units` object and a set of definitions declared in this repo. +You can use this object to handle any conversions or any other interaction with those units. + +```js +import { Units } from '@harvest-profit/units'; + +const amount = new Units(1, 'gallon'); +amount.to('pints').toNumber() // 8 pints + +amount.isCompatible('lbs') // false + + +// Can use any different name. If a name is missing, just add it to the aliases in the definition in a PR +const gal = new Units(1, 'gal'); +const gallon = new Units(1, 'gallon'); +const gallons = new Units(1, 'gallons'); + +Units.selectableUnits('liquid') // ['gallons', 'floz', 'milliliters', ...] + +Units.isCompatible('g', 'lb') // true +Units.isCompatible(gallon, 'lb') // false +Units.isCompatible(gal, gallon) // true ``` ## Development diff --git a/package.json b/package.json index eaac5cb..ace6717 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@harvest-profit/units", - "version": "1.4.0", + "version": "1.4.1", "description": "Units helper for Harvest Profit javascript applications", "main": "dist/index.js", "repository": "https://github.com/HarvestProfit/harvest-profit-units", diff --git a/src/Units.js b/src/Units.js index 244add5..01ddba7 100644 --- a/src/Units.js +++ b/src/Units.js @@ -1,73 +1,5 @@ -import liquidDefinitions from './definitions/liquid'; -import solidDefinitions from './definitions/solid'; -import seedDefinitions from './definitions/seed'; -import yieldDefinitions from './definitions/yield'; - -const inflatedUnits = {}; - -class UnitRedefinitionError extends Error { - constructor(message) { - super(message); - this.name = 'UnitRedefinitionError'; - } -} - -class UndefinedUnitError extends Error { - constructor(message) { - super(message); - this.name = 'UndefinedUnitError'; - } -} - -class UnitCompatibilityError extends Error { - constructor(message) { - super(message); - this.name = 'UnitCompatibilityError'; - } -} - -function addUnitDefinitionWithoutError(group, name, value, primaryName, fullName) { - if (!inflatedUnits[name]) { - inflatedUnits[name] = { - group, - value, - primaryName, - fullName - }; - } - - -} -function addUnitDefinition(group, name, value, primaryName, fullName) { - if (inflatedUnits[name]) { - throw new UnitRedefinitionError(`${name} is already a defined unit. Do not redefine units`); - } - - addUnitDefinitionWithoutError(group, name, value, primaryName, fullName); -} - -function inflateUnits(compatibilityGroup, definitions) { - Object.keys(definitions).forEach((unitName) => { - const definition = definitions[unitName]; - addUnitDefinition(compatibilityGroup, unitName, definition.value, unitName, definition.name); - addUnitDefinitionWithoutError(compatibilityGroup, definition.name, definition.value, unitName, definition.name); - const plural = definition.name + (definition.name[definition.name.length - 1] === 's' ? 'es' : 's'); - addUnitDefinitionWithoutError(compatibilityGroup, plural, definition.value, unitName, definition.name); - - - (definition.aliases || []).forEach((aliasName) => { - addUnitDefinition(compatibilityGroup, aliasName, definition.value, unitName, definition.name); - const plural = aliasName + (aliasName[aliasName.length - 1] === 's' ? 'es' : 's'); - addUnitDefinitionWithoutError(compatibilityGroup, plural, definition.value, unitName, definition.name); - }); - }); -} - -inflateUnits('liquid', liquidDefinitions); -inflateUnits('weight', solidDefinitions); -inflateUnits('seed', seedDefinitions); -inflateUnits('yield', yieldDefinitions); - +import { inflatedUnits, selectableUnitsByGroup } from './definitions'; +import { UndefinedUnitError, UnitCompatibilityError } from './errors'; function retrieveUnit(unit) { if (typeof unit === 'string') return unit; @@ -84,6 +16,8 @@ function checkUnitValidity(unitArgument) { } +// This class is used to operate with units. Providing invalid units to its functions will result in +// it throwing a UndefinedUnitError class Units { constructor(value, unit) { checkUnitValidity(unit); @@ -95,6 +29,13 @@ class Units { } } + // Given a compatibility group name, it will return the common units in that group. + // group names include: weight, liquid, seed, and yield + static selectableUnits(group) { + return selectableUnitsByGroup[group]; + } + + // Checks if 2 units are compatible. Units may be a Units object. static isCompatible(unit1, unit2) { checkUnitValidity(unit1); checkUnitValidity(unit2); @@ -102,14 +43,17 @@ class Units { return inflatedUnits[retrieveUnit(unit1)].group === inflatedUnits[retrieveUnit(unit2)].group; } - equalBase = (unit) => this.isCompatible(unit); + // Checks if a unit is compatible with the object's unit. isCompatible(unit) { checkUnitValidity(unit); return inflatedUnits[this.unit].group === inflatedUnits[retrieveUnit(unit)].group; } - to(unitArgument, conversionObject = null) { + // Converts the object's value into the provided unit. The provided unit may be a Units object. + // This will throw a UnitCompatibilityError when a unit is not compatible. + // returns a Units object (so you can chain) + to(unitArgument) { const unit = retrieveUnit(unitArgument) if (!this.isCompatible(unit)) { throw new UnitCompatibilityError(`${unit} is not compatible with ${this.unit}`); @@ -120,10 +64,12 @@ class Units { return new Units(convertedValue, unit); } + // Returns the numeric value of the unit. toNumber() { return this.value; } + // Prints out the unit as "value unit" toString() { return `${this.value} ${this.unit}` } diff --git a/src/UnitsHelper.js b/src/UnitsHelper.js index 7409f20..eda08bd 100644 --- a/src/UnitsHelper.js +++ b/src/UnitsHelper.js @@ -1,40 +1,12 @@ import Units from './Units'; -export const availableBushelUnits = [ - 'bushels', -]; - -export const availableSeedUnits = [ - 'seeds', - 'bags', - 'units - 130k', - 'units - 140k', -]; - -export const availableSolidUnits = [ - 'lbs', - 'oz', - 'tons', - 'grams', - 'kilograms', - 'metric tons', -]; - -export const availableLiquidUnits = [ - 'gallons', - 'floz', - 'liters', - 'milliliters', - 'pints', - 'quarts', -]; - -export class ConversionError extends Error { - constructor(message) { - super(message); - this.name = 'ConversionError'; - } -} +export const availableBushelUnits = Units.selectableUnits('yield'); + +export const availableSeedUnits = Units.selectableUnits('seed'); + +export const availableSolidUnits = Units.selectableUnits('weight'); + +export const availableLiquidUnits = Units.selectableUnits('liquid'); export default class UnitsHelper { @@ -123,10 +95,7 @@ export default class UnitsHelper { static convertToUnit(amount, fromUnit, toUnit) { const parsedFromUnit = this.parseUnit(fromUnit); const parsedToUnit = this.parseUnit(toUnit); - if (UnitsHelper.isCompatibleUnit(parsedFromUnit, parsedToUnit)) { - return new Units(amount, parsedFromUnit).to(parsedToUnit).toNumber(); - } - throw new ConversionError(`Cannot convert ${parsedFromUnit} to ${parsedToUnit}`); + return new Units(amount, parsedFromUnit).to(parsedToUnit).toNumber(); } static convertToGallons(amount, unit) { diff --git a/src/definitions/index.js b/src/definitions/index.js new file mode 100644 index 0000000..022d1e8 --- /dev/null +++ b/src/definitions/index.js @@ -0,0 +1,58 @@ +import liquidDefinitions from './liquid'; +import solidDefinitions from './solid'; +import seedDefinitions from './seed'; +import yieldDefinitions from './yield'; +import { UnitRedefinitionError, UndefinedUnitError } from '../errors'; + +export const inflatedUnits = {}; +export const selectableUnitsByGroup = {}; + +function addUnitDefinitionWithoutError(group, name, value, primaryName, fullName) { + if (!inflatedUnits[name]) { + inflatedUnits[name] = { + group, + value, + primaryName, + fullName + }; + } +} + +function addUnitDefinition(group, name, value, primaryName, fullName) { + if (inflatedUnits[name]) { + throw new UnitRedefinitionError(`${name} is already a defined unit. Do not redefine units`); + } + + addUnitDefinitionWithoutError(group, name, value, primaryName, fullName); +} + +// Used to expand the definitions provided into a more robust object with multiple lookup keys. +// allows looking up units by their short name, plural name, or any alias. +// Additionally, it builds a list of common selectable units. +function inflateUnits(compatibilityGroup, definitions) { + Object.keys(definitions).forEach((unitName) => { + const definition = definitions[unitName]; + addUnitDefinition(compatibilityGroup, unitName, definition.value, unitName, definition.name); + addUnitDefinitionWithoutError(compatibilityGroup, definition.name, definition.value, unitName, definition.name); + const plural = definition.name + (definition.name[definition.name.length - 1] === 's' ? 'es' : 's'); + addUnitDefinitionWithoutError(compatibilityGroup, plural, definition.value, unitName, definition.name); + + + (definition.aliases || []).forEach((aliasName) => { + addUnitDefinition(compatibilityGroup, aliasName, definition.value, unitName, definition.name); + const plural = aliasName + (aliasName[aliasName.length - 1] === 's' ? 'es' : 's'); + addUnitDefinitionWithoutError(compatibilityGroup, plural, definition.value, unitName, definition.name); + }); + + if (definition.selectableAs) { + selectableUnitsByGroup[compatibilityGroup] = selectableUnitsByGroup[compatibilityGroup] || []; + selectableUnitsByGroup[compatibilityGroup].push(definition.selectableAs); + } + }); +} + + +inflateUnits('liquid', liquidDefinitions); +inflateUnits('weight', solidDefinitions); +inflateUnits('seed', seedDefinitions); +inflateUnits('yield', yieldDefinitions); diff --git a/src/definitions/liquid.js b/src/definitions/liquid.js index 43d5ccc..be02991 100644 --- a/src/definitions/liquid.js +++ b/src/definitions/liquid.js @@ -1,28 +1,42 @@ +/* + key = common name/short name + name = full name of unit. A plural version of this is also added + aliases = other names for unit (tonnes and metric tons for example). Plural versions of these are also added + value = conversion number. If liters have a value of 1, then milliliters have a value of 0.001 (there is 0.001 liters in 1 milliliter) + selectableAs = the name that shows up in the list of units that we want to be able to select from. Not every unit should go on here, just the common ones +*/ + export default { l: { name: 'liter', aliases: ['litre'], value: 1, + selectableAs: 'liters', }, ml: { name: 'milliliter', value: 0.001, + selectableAs: 'milliliters', }, pt: { name: 'pint', value: 0.473176473, + selectableAs: 'pints', }, qt: { name: 'quart', value: 0.946352946, + selectableAs: 'quarts', }, gal: { name: 'gallon', value: 3.785411784, + selectableAs: 'gallons', }, floz: { name: 'fluid ounce', aliases: ['fl oz'], value: 0.02957353, + selectableAs: 'floz', } } diff --git a/src/definitions/seed.js b/src/definitions/seed.js index 2a9d728..9de1adf 100644 --- a/src/definitions/seed.js +++ b/src/definitions/seed.js @@ -1,20 +1,32 @@ +/* + key = common name/short name + name = full name of unit. A plural version of this is also added + aliases = other names for unit (tonnes and metric tons for example). Plural versions of these are also added + value = conversion number. If liters have a value of 1, then milliliters have a value of 0.001 (there is 0.001 liters in 1 milliliter) + selectableAs = the name that shows up in the list of units that we want to be able to select from. Not every unit should go on here, just the common ones +*/ + export default { seed: { name: 'seed', value: 1, + selectableAs: 'seeds' }, bag: { name: 'bag', value: 80000, + selectableAs: 'bags' }, 'units - 130k': { name: 'units - 130k', aliases: ['units130k'], value: 130000, + selectableAs: 'units - 130k' }, 'units - 140k': { name: 'units - 140k', aliases: ['units140k'], value: 140000, + selectableAs: 'units - 140k', }, } diff --git a/src/definitions/solid.js b/src/definitions/solid.js index 372863c..b76cfcf 100644 --- a/src/definitions/solid.js +++ b/src/definitions/solid.js @@ -1,7 +1,16 @@ +/* + key = common name/short name + name = full name of unit. A plural version of this is also added + aliases = other names for unit (tonnes and metric tons for example). Plural versions of these are also added + value = conversion number. If liters have a value of 1, then milliliters have a value of 0.001 (there is 0.001 liters in 1 milliliter) + selectableAs = the name that shows up in the list of units that we want to be able to select from. Not every unit should go on here, just the common ones +*/ + export default { g: { name: 'gram', value: 1, + selectableAs: 'grams', }, mg: { name: 'milligram', @@ -10,23 +19,28 @@ export default { kg: { name: 'kilogram', value: 1000, + selectableAs: 'kilograms', }, t: { name: 'metric ton', aliases: ['tonne'], value: 1000000, + selectableAs: 'metric tons', }, ton: { name: 'ton', value: 907184.74, + selectableAs: 'tons', }, oz: { name: 'ounce', value: 28.349523125, + selectableAs: 'oz', }, lbs: { name: 'pound', aliases: ['lb'], - value: 453.592375 + value: 453.592375, + selectableAs: 'lbs', }, } diff --git a/src/definitions/yield.js b/src/definitions/yield.js index 3e8d8d0..0e37dbe 100644 --- a/src/definitions/yield.js +++ b/src/definitions/yield.js @@ -1,6 +1,16 @@ +/* + key = common name/short name + name = full name of unit. A plural version of this is also added + aliases = other names for unit (tonnes and metric tons for example). Plural versions of these are also added + value = conversion number. If liters have a value of 1, then milliliters have a value of 0.001 (there is 0.001 liters in 1 milliliter) + selectableAs = the name that shows up in the list of units that we want to be able to select from. Not every unit should go on here, just the common ones +*/ + + export default { bushel: { name: 'bushel', value: 1, + selectableAs: 'bushels' }, } diff --git a/src/errors.js b/src/errors.js new file mode 100644 index 0000000..0acb84f --- /dev/null +++ b/src/errors.js @@ -0,0 +1,23 @@ +// Thrown when defining duplicate units in the definitions +export class UnitRedefinitionError extends Error { + constructor(message) { + super(message); + this.name = 'UnitRedefinitionError'; + } +} + +// Thrown when providing unknown units to the Units object +export class UndefinedUnitError extends Error { + constructor(message) { + super(message); + this.name = 'UndefinedUnitError'; + } +} + +// Thrown when providing incompatible units to the Units object +export class UnitCompatibilityError extends Error { + constructor(message) { + super(message); + this.name = 'UnitCompatibilityError'; + } +} diff --git a/test/UnitsHelper.test.js b/test/UnitsHelper.test.js index 5a589b3..3d063d2 100644 --- a/test/UnitsHelper.test.js +++ b/test/UnitsHelper.test.js @@ -116,6 +116,10 @@ describe('UnitsHelper', () => { it('should list available solid units when unit is "lb"', () => { const units = UnitsHelper.listAvailableUnits({ units: 'lb' }); expect(units).toEqual(availableSolidUnits); + expect(units).toContain('lbs'); + expect(units).toContain('grams'); + expect(units).toContain('tons'); + expect(units).toContain('metric tons'); }); it('should list available bushel units when unit is "bushel"', () => { @@ -126,11 +130,19 @@ describe('UnitsHelper', () => { it('should list available liquid units when unit is "liter"', () => { const units = UnitsHelper.listAvailableUnits({ units: 'liter' }); expect(units).toEqual(availableLiquidUnits); + expect(units).toContain('milliliters'); + expect(units).toContain('gallons'); + expect(units).toContain('liters'); + expect(units).toContain('floz'); }); it('should list available seed units when unit is "seed"', () => { const units = UnitsHelper.listAvailableUnits({ units: 'seed' }); expect(units).toEqual(availableSeedUnits); + expect(units).toContain('seeds'); + expect(units).toContain('units - 130k'); + expect(units).toContain('bags'); + expect(units).toContain('units - 140k'); }); it('should list no available seed units when unit is "custom"', () => {