houdini/app/javascript/common/money.ts

271 lines
6.7 KiB
TypeScript
Raw Normal View History

2020-06-24 19:22:46 +00:00
// License: LGPL-3.0-or-later
// based upon https://github.com/davidkalosi/js-money
2020-06-26 23:19:08 +00:00
import isFunction from 'lodash/isFunction';
2020-06-24 19:22:46 +00:00
2020-06-26 23:19:08 +00:00
const assertSameCurrency = function (left: Money, right: Money) {
2020-06-24 19:22:46 +00:00
if (left.currency !== right.currency)
throw new Error('Different currencies');
};
2020-06-26 23:19:08 +00:00
const assertType = function (other: unknown) {
2020-06-24 19:22:46 +00:00
if (!(other instanceof Money))
throw new TypeError('Instance of Money required');
};
2020-06-26 23:19:08 +00:00
const assertOperand = function (operand: unknown) {
if (typeof operand !== 'number' || isNaN(operand) && !isFinite(operand))
2020-06-24 19:22:46 +00:00
throw new TypeError('Operand must be a number');
};
type MoneyAsJson = {amount: number, currency: string}
2020-06-24 19:22:46 +00:00
/**
* Represents a monetary amount. For safety, all Money objects are immutable. All of the functions in this class create a new Money object.
*
* To create a new Money object is to use the `fromCents` function.
* @export
* @class Money
*/
export class Money {
readonly currency:string
protected constructor(readonly amount: number, currency: string) {
2020-06-26 23:19:08 +00:00
this.currency = currency.toLowerCase();
2020-06-24 21:42:03 +00:00
const methodsToBind = [this.equals, this.add, this.subtract, this.multiply, this.divide, this.allocate,
2020-06-24 19:22:46 +00:00
this.compare, this.greaterThan, this.greaterThanOrEqual, this.lessThan,
this.lessThanOrEqual, this.isZero, this.isPositive, this.isNegative,
2020-06-26 23:19:08 +00:00
this.toJSON];
methodsToBind.forEach((func) => Object.bind(func));
2020-06-24 19:22:46 +00:00
Object.freeze(this);
}
/**
* Create a `Money` object with the given number of cents and the ISO currency unit
* @static
* @param {number} amount
2020-06-24 19:22:46 +00:00
* @param {string} currency
* @return Money
* @memberof Money
*/
static fromCents(amount:MoneyAsJson): Money;
static fromCents(amount:Money): Money;
static fromCents(amount: number, currency: string) : Money;
static fromCents(amount: number|Money|MoneyAsJson, currency?: string): Money {
if (typeof amount === 'number')
return new Money(amount, currency);
if (amount instanceof Money)
return new Money(amount.amount, amount.currency);
else
2020-06-26 23:19:08 +00:00
return new Money(amount.amount, amount.currency);
2020-06-24 19:22:46 +00:00
}
2020-06-24 19:22:46 +00:00
/**
* Create a `Money` object with the given number if smallest monetary units and the ISO currency. Another name for the `fromCents` function.
* @static
* @memberof Money
*/
static fromSMU=Money.fromCents
/**
* Returns true if the two instances of Money are equal, false otherwise.
*
* @param {Money} other
* @returns {Boolean}
*/
equals(other: Money): boolean {
2020-06-26 23:19:08 +00:00
2020-06-24 19:22:46 +00:00
assertType(other);
2020-06-26 23:19:08 +00:00
return this.amount === other.amount &&
this.currency === other.currency;
}
2020-06-24 19:22:46 +00:00
/**
* Adds the two objects together creating a new Money instance that holds the result of the operation.
*
* @param {Money} other
* @returns {Money}
*/
add(other: Money): Money {
2020-06-26 23:19:08 +00:00
2020-06-24 19:22:46 +00:00
assertType(other);
2020-06-26 23:19:08 +00:00
assertSameCurrency(this, other);
2020-06-24 19:22:46 +00:00
2020-06-26 23:19:08 +00:00
return new Money(this.amount + other.amount, this.currency);
}
2020-06-24 19:22:46 +00:00
/**
* Subtracts the two objects creating a new Money instance that holds the result of the operation.
*
* @param {Money} other
* @returns {Money}
*/
subtract(other: Money): Money {
2020-06-26 23:19:08 +00:00
2020-06-24 19:22:46 +00:00
assertType(other);
2020-06-26 23:19:08 +00:00
assertSameCurrency(this, other);
2020-06-24 19:22:46 +00:00
2020-06-26 23:19:08 +00:00
return new Money(this.amount - other.amount, this.currency);
}
2020-06-24 19:22:46 +00:00
/**
* Multiplies the object by the multiplier returning a new Money instance that holds the result of the operation.
*
2020-06-26 23:19:08 +00:00
* @param {number} multiplier
2020-06-24 19:22:46 +00:00
* @param {(x:number) => number} [fn=Math.round]
* @returns {Money}
*/
2020-06-26 23:19:08 +00:00
multiply(multiplier: number, roundingFunction: (x:number) => number): Money {
if (!isFunction(roundingFunction))
roundingFunction = Math.round;
2020-06-24 19:22:46 +00:00
assertOperand(multiplier);
2020-06-26 23:19:08 +00:00
const amount = roundingFunction(this.amount * multiplier);
2020-06-24 19:22:46 +00:00
return new Money(amount, this.currency);
2020-06-26 23:19:08 +00:00
}
2020-06-24 19:22:46 +00:00
/**
* Divides the object by the multiplier returning a new Money instance that holds the result of the operation.
*
* @param {Number} divisor
* @param {(x:number) => number} [fn=Math.round]
* @returns {Money}
*/
divide(divisor: number, fn?: (x: number) => number): Money {
2020-06-24 21:42:03 +00:00
if (!isFunction(fn))
2020-06-24 19:22:46 +00:00
fn = Math.round;
assertOperand(divisor);
2020-06-26 23:19:08 +00:00
const amount = fn(this.amount / divisor);
2020-06-24 19:22:46 +00:00
return new Money(amount, this.currency);
2020-06-26 23:19:08 +00:00
}
2020-06-24 19:22:46 +00:00
/**
* Allocates fund bases on the ratios provided returing an array of objects as a product of the allocation.
*
* @param {Array} other
* @param {Money[]}
*/
allocate(ratios: number[]): Money[] {
2020-06-26 23:19:08 +00:00
let remainder = this.amount;
const results: Money[] = [];
let total = 0;
2020-06-24 19:22:46 +00:00
ratios.forEach(function (ratio) {
total += ratio;
});
ratios.forEach(function (ratio) {
2020-06-26 23:19:08 +00:00
const share = Math.floor(this.amount * ratio / total);
results.push(new Money(share, this.currency));
2020-06-24 19:22:46 +00:00
remainder -= share;
});
2020-06-26 23:19:08 +00:00
for (let i = 0; remainder > 0; i++) {
results[i] = new Money(results[i].amount + 1, results[i].currency);
2020-06-24 19:22:46 +00:00
remainder--;
}
return results;
2020-06-26 23:19:08 +00:00
}
2020-06-24 19:22:46 +00:00
/**
* Compares two instances of Money.
*
* @param {Money} other
* @returns {Number}
*/
compare(other: Money): number {
2020-06-26 23:19:08 +00:00
2020-06-24 19:22:46 +00:00
assertType(other);
2020-06-26 23:19:08 +00:00
assertSameCurrency(this, other);
2020-06-24 19:22:46 +00:00
2020-06-26 23:19:08 +00:00
if (this.amount === other.amount)
2020-06-24 19:22:46 +00:00
return 0;
2020-06-26 23:19:08 +00:00
return this.amount > other.amount ? 1 : -1;
}
2020-06-24 19:22:46 +00:00
/**
* Checks whether the value represented by this object is greater than the other.
*
* @param {Money} other
* @returns {boolean}
*/
greaterThan(other: Money): boolean {
return 1 === this.compare(other);
2020-06-26 23:19:08 +00:00
}
2020-06-24 19:22:46 +00:00
/**
* Checks whether the value represented by this object is greater or equal to the other.
*
* @param {Money} other
* @returns {boolean}
*/
greaterThanOrEqual(other: Money): boolean {
return 0 <= this.compare(other);
2020-06-26 23:19:08 +00:00
}
2020-06-24 19:22:46 +00:00
/**
* Checks whether the value represented by this object is less than the other.
*
* @param {Money} other
* @returns {boolean}
*/
lessThan(other: Money): boolean {
return -1 === this.compare(other);
2020-06-26 23:19:08 +00:00
}
2020-06-24 19:22:46 +00:00
/**
* Checks whether the value represented by this object is less than or equal to the other.
*
* @param {Money} other
* @returns {boolean}
*/
lessThanOrEqual(other: Money): boolean {
return 0 >= this.compare(other);
2020-06-26 23:19:08 +00:00
}
2020-06-24 19:22:46 +00:00
/**
* Returns true if the amount is zero.
*
* @returns {boolean}
*/
isZero(): boolean {
return this.amount === 0;
2020-06-26 23:19:08 +00:00
}
2020-06-24 19:22:46 +00:00
/**
* Returns true if the amount is positive.
*
* @returns {boolean}
*/
isPositive(): boolean {
return this.amount > 0;
2020-06-26 23:19:08 +00:00
}
2020-06-24 19:22:46 +00:00
isNegative(): boolean {
return this.amount < 0;
2020-06-26 23:19:08 +00:00
}
2020-06-24 19:22:46 +00:00
/**
* Returns a serialised version of the instance.
*
* @returns {{amount: number, currency: string}}
*/
toJSON(): MoneyAsJson {
2020-06-24 19:22:46 +00:00
return {
amount: this.amount,
2020-06-24 19:22:46 +00:00
currency: this.currency
};
2020-06-26 23:19:08 +00:00
}
2020-06-24 19:22:46 +00:00
}