dinero.js 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,59 @@
1
+ import { isPercentage, areValidRatios } from './helpers'
2
+
3
+ /**
4
+ * Performs an assertion.
5
+ * @ignore
6
+ *
7
+ * @param {Boolean} condition - The expression to assert.
8
+ * @param {String} errorMessage - The message to throw if the assertion fails
9
+ * @param {ErrorConstructor} [ErrorType=Error] - The error to throw if the assertion fails.
10
+ *
11
+ * @throws {Error} If `condition` returns `false`.
12
+ */
13
+ export function assert(condition, errorMessage, ErrorType = Error) {
14
+ if (!condition) throw new ErrorType(errorMessage)
15
+ }
16
+
17
+ /**
18
+ * Asserts a value is a percentage.
19
+ * @ignore
20
+ *
21
+ * @param {} percentage - The percentage to test.
22
+ *
23
+ * @throws {RangeError} If `percentage` is out of range.
24
+ */
25
+ export function assertPercentage(percentage) {
26
+ assert(
27
+ isPercentage(percentage),
28
+ 'You must provide a numeric value between 0 and 100.',
29
+ RangeError
30
+ )
31
+ }
32
+
33
+ /**
34
+ * Asserts an array of ratios is valid.
35
+ * @ignore
36
+ *
37
+ * @param {} ratios - The ratios to test.
38
+ *
39
+ * @throws {TypeError} If `ratios` are invalid.
40
+ */
41
+ export function assertValidRatios(ratios) {
42
+ assert(
43
+ areValidRatios(ratios),
44
+ 'You must provide a non-empty array of numeric values greater than 0.',
45
+ TypeError
46
+ )
47
+ }
48
+
49
+ /**
50
+ * Asserts a value is an integer.
51
+ * @ignore
52
+ *
53
+ * @param {} number - The value to test.
54
+ *
55
+ * @throws {TypeError}
56
+ */
57
+ export function assertInteger(number) {
58
+ assert(Number.isInteger(number), 'You must provide an integer.', TypeError)
59
+ }
@@ -0,0 +1,122 @@
1
+ import { isEven, isFloat, countFractionDigits, isHalf } from './helpers'
2
+
3
+ export default function Calculator() {
4
+ const floatMultiply = (a, b) => {
5
+ const getFactor = number => Math.pow(10, countFractionDigits(number))
6
+ const factor = Math.max(getFactor(a), getFactor(b))
7
+ return (Math.round(a * factor) * Math.round(b * factor)) / (factor * factor)
8
+ }
9
+
10
+ const roundingModes = {
11
+ HALF_ODD(number) {
12
+ const rounded = Math.round(number)
13
+ return isHalf(number)
14
+ ? isEven(rounded)
15
+ ? rounded - 1
16
+ : rounded
17
+ : rounded
18
+ },
19
+ HALF_EVEN(number) {
20
+ const rounded = Math.round(number)
21
+ return isHalf(number)
22
+ ? isEven(rounded)
23
+ ? rounded
24
+ : rounded - 1
25
+ : rounded
26
+ },
27
+ HALF_UP(number) {
28
+ return Math.round(number)
29
+ },
30
+ HALF_DOWN(number) {
31
+ return isHalf(number) ? Math.floor(number) : Math.round(number)
32
+ },
33
+ HALF_TOWARDS_ZERO(number) {
34
+ return isHalf(number)
35
+ ? Math.sign(number) * Math.floor(Math.abs(number))
36
+ : Math.round(number)
37
+ },
38
+ HALF_AWAY_FROM_ZERO(number) {
39
+ return isHalf(number)
40
+ ? Math.sign(number) * Math.ceil(Math.abs(number))
41
+ : Math.round(number)
42
+ },
43
+ DOWN(number) {
44
+ return Math.floor(number)
45
+ }
46
+ }
47
+
48
+ return {
49
+ /**
50
+ * Returns the sum of two numbers.
51
+ * @ignore
52
+ *
53
+ * @param {Number} a - The first number to add.
54
+ * @param {Number} b - The second number to add.
55
+ *
56
+ * @return {Number}
57
+ */
58
+ add(a, b) {
59
+ return a + b
60
+ },
61
+ /**
62
+ * Returns the difference of two numbers.
63
+ * @ignore
64
+ *
65
+ * @param {Number} a - The first number to subtract.
66
+ * @param {Number} b - The second number to subtract.
67
+ *
68
+ * @return {Number}
69
+ */
70
+ subtract(a, b) {
71
+ return a - b
72
+ },
73
+ /**
74
+ * Returns the product of two numbers.
75
+ * @ignore
76
+ *
77
+ * @param {Number} a - The first number to multiply.
78
+ * @param {Number} b - The second number to multiply.
79
+ *
80
+ * @return {Number}
81
+ */
82
+ multiply(a, b) {
83
+ return isFloat(a) || isFloat(b) ? floatMultiply(a, b) : a * b
84
+ },
85
+ /**
86
+ * Returns the quotient of two numbers.
87
+ * @ignore
88
+ *
89
+ * @param {Number} a - The first number to divide.
90
+ * @param {Number} b - The second number to divide.
91
+ *
92
+ * @return {Number}
93
+ */
94
+ divide(a, b) {
95
+ return a / b
96
+ },
97
+ /**
98
+ * Returns the remainder of two numbers.
99
+ * @ignore
100
+ *
101
+ * @param {Number} a - The first number to divide.
102
+ * @param {Number} b - The second number to divide.
103
+ *
104
+ * @return {Number}
105
+ */
106
+ modulo(a, b) {
107
+ return a % b
108
+ },
109
+ /**
110
+ * Returns a rounded number based off a specific rounding mode.
111
+ * @ignore
112
+ *
113
+ * @param {Number} number - The number to round.
114
+ * @param {String} [roundingMode='HALF_EVEN'] - The rounding mode to use.
115
+ *
116
+ * @returns {Number}
117
+ */
118
+ round(number, roundingMode = 'HALF_EVEN') {
119
+ return roundingModes[roundingMode](number)
120
+ }
121
+ }
122
+ }
@@ -0,0 +1,38 @@
1
+ import { getJSON, flattenObject, isThenable } from './helpers'
2
+
3
+ export default function CurrencyConverter(options) {
4
+ /* istanbul ignore next */
5
+ const mergeTags = (string = '', tags) => {
6
+ for (const tag in tags) {
7
+ string = string.replace(`{{${tag}}}`, tags[tag])
8
+ }
9
+ return string
10
+ }
11
+
12
+ /* istanbul ignore next */
13
+ const getRatesFromRestApi = (from, to) =>
14
+ getJSON(mergeTags(options.endpoint, { from, to }), {
15
+ headers: options.headers
16
+ })
17
+
18
+ return {
19
+ /**
20
+ * Returns the exchange rate.
21
+ * @ignore
22
+ *
23
+ * @param {String} from - The base currency.
24
+ * @param {String} to - The destination currency.
25
+ *
26
+ * @return {Promise}
27
+ */
28
+ getExchangeRate(from, to) {
29
+ return (isThenable(options.endpoint)
30
+ ? options.endpoint
31
+ : getRatesFromRestApi(from, to)
32
+ ).then(
33
+ data =>
34
+ flattenObject(data)[mergeTags(options.propertyPath, { from, to })]
35
+ )
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,76 @@
1
+ import Calculator from './calculator'
2
+ import { isUndefined } from './helpers'
3
+
4
+ const calculator = Calculator()
5
+
6
+ export default function Format(format) {
7
+ const matches = /^(?:(\$|USD)?0(?:(,)0)?(\.)?(0+)?|0(?:(,)0)?(\.)?(0+)?\s?(dollar)?)$/gm.exec(
8
+ format
9
+ )
10
+
11
+ return {
12
+ /**
13
+ * Returns the matches.
14
+ * @ignore
15
+ *
16
+ * @return {Array}
17
+ */
18
+ getMatches() {
19
+ return matches !== null
20
+ ? matches.slice(1).filter(match => !isUndefined(match))
21
+ : []
22
+ },
23
+ /**
24
+ * Returns the amount of fraction digits to display.
25
+ * @ignore
26
+ *
27
+ * @return {Number}
28
+ */
29
+ getMinimumFractionDigits() {
30
+ const decimalPosition = match => match === '.'
31
+ return !isUndefined(this.getMatches().find(decimalPosition))
32
+ ? this.getMatches()[
33
+ calculator.add(this.getMatches().findIndex(decimalPosition), 1)
34
+ ].split('').length
35
+ : 0
36
+ },
37
+ /**
38
+ * Returns the currency display mode.
39
+ * @ignore
40
+ *
41
+ * @return {String}
42
+ */
43
+ getCurrencyDisplay() {
44
+ const modes = {
45
+ USD: 'code',
46
+ dollar: 'name',
47
+ $: 'symbol'
48
+ }
49
+ return modes[
50
+ this.getMatches().find(match => {
51
+ return match === 'USD' || match === 'dollar' || match === '$'
52
+ })
53
+ ]
54
+ },
55
+ /**
56
+ * Returns the formatting style.
57
+ * @ignore
58
+ *
59
+ * @return {String}
60
+ */
61
+ getStyle() {
62
+ return !isUndefined(this.getCurrencyDisplay(this.getMatches()))
63
+ ? 'currency'
64
+ : 'decimal'
65
+ },
66
+ /**
67
+ * Returns whether grouping should be used or not.
68
+ * @ignore
69
+ *
70
+ * @return {Boolean}
71
+ */
72
+ getUseGrouping() {
73
+ return !isUndefined(this.getMatches().find(match => match === ','))
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Returns whether a value is numeric.
3
+ * @ignore
4
+ *
5
+ * @param {} value - The value to test.
6
+ *
7
+ * @return {Boolean}
8
+ */
9
+ export function isNumeric(value) {
10
+ return !isNaN(parseInt(value)) && isFinite(value)
11
+ }
12
+
13
+ /**
14
+ * Returns whether a value is a percentage.
15
+ * @ignore
16
+ *
17
+ * @param {} percentage - The percentage to test.
18
+ *
19
+ * @return {Boolean}
20
+ */
21
+ export function isPercentage(percentage) {
22
+ return isNumeric(percentage) && percentage <= 100 && percentage >= 0
23
+ }
24
+
25
+ /**
26
+ * Returns whether an array of ratios is valid.
27
+ * @ignore
28
+ *
29
+ * @param {} ratios - The ratios to test.
30
+ *
31
+ * @return {Boolean}
32
+ */
33
+ export function areValidRatios(ratios) {
34
+ return (
35
+ ratios.length > 0 &&
36
+ ratios.every(ratio => ratio >= 0) &&
37
+ ratios.some(ratio => ratio > 0)
38
+ )
39
+ }
40
+
41
+ /**
42
+ * Returns whether a value is even.
43
+ * @ignore
44
+ *
45
+ * @param {Number} value - The value to test.
46
+ *
47
+ * @return {Boolean}
48
+ */
49
+ export function isEven(value) {
50
+ return value % 2 === 0
51
+ }
52
+
53
+ /**
54
+ * Returns whether a value is a float.
55
+ * @ignore
56
+ *
57
+ * @param {} value - The value to test.
58
+ *
59
+ * @return {Boolean}
60
+ */
61
+ export function isFloat(value) {
62
+ return isNumeric(value) && !Number.isInteger(value)
63
+ }
64
+
65
+ /**
66
+ * Returns how many fraction digits a number has.
67
+ * @ignore
68
+ *
69
+ * @param {Number} [number=0] - The number to test.
70
+ *
71
+ * @return {Number}
72
+ */
73
+ export function countFractionDigits(number = 0) {
74
+ const stringRepresentation = number.toString()
75
+ if (stringRepresentation.indexOf('e-') > 0) {
76
+ // It's too small for a normal string representation, e.g. 1e-7 instead of 0.00000001
77
+ return parseInt(stringRepresentation.split('e-')[1])
78
+ } else {
79
+ const fractionDigits = stringRepresentation.split('.')[1]
80
+ return fractionDigits ? fractionDigits.length : 0
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Returns whether a number is half.
86
+ * @ignore
87
+ *
88
+ * @param {Number} number - The number to test.
89
+ *
90
+ * @return {Number}
91
+ */
92
+ export function isHalf(number) {
93
+ return Math.abs(number) % 1 === 0.5
94
+ }
95
+
96
+ /**
97
+ * Fetches a JSON resource.
98
+ * @ignore
99
+ *
100
+ * @param {String} url - The resource to fetch.
101
+ * @param {Object} [options.headers] - The headers to pass.
102
+ *
103
+ * @throws {Error} If `request.status` is lesser than 200 or greater or equal to 400.
104
+ * @throws {Error} If network fails.
105
+ *
106
+ * @return {JSON}
107
+ */
108
+ export function getJSON(url, options = {}) {
109
+ return new Promise((resolve, reject) => {
110
+ const request = Object.assign(new XMLHttpRequest(), {
111
+ onreadystatechange() {
112
+ if (request.readyState === 4) {
113
+ if (request.status >= 200 && request.status < 400)
114
+ resolve(JSON.parse(request.responseText))
115
+ else reject(new Error(request.statusText))
116
+ }
117
+ },
118
+ onerror() {
119
+ reject(new Error('Network error'))
120
+ }
121
+ })
122
+
123
+ request.open('GET', url, true)
124
+ setXHRHeaders(request, options.headers)
125
+ request.send()
126
+ })
127
+ }
128
+
129
+ /**
130
+ * Returns an XHR object with attached headers.
131
+ * @ignore
132
+ *
133
+ * @param {XMLHttpRequest} xhr - The XHR request to set headers to.
134
+ * @param {Object} headers - The headers to set.
135
+ *
136
+ * @return {XMLHttpRequest}
137
+ */
138
+ export function setXHRHeaders(xhr, headers = {}) {
139
+ for (const header in headers) xhr.setRequestHeader(header, headers[header])
140
+ return xhr
141
+ }
142
+
143
+ /**
144
+ * Returns whether a value is undefined.
145
+ * @ignore
146
+ *
147
+ * @param {} value - The value to test.
148
+ *
149
+ * @return {Boolean}
150
+ */
151
+ export function isUndefined(value) {
152
+ return typeof value === 'undefined'
153
+ }
154
+
155
+ /**
156
+ * Returns an object flattened to one level deep.
157
+ * @ignore
158
+ *
159
+ * @param {Object} object - The object to flatten.
160
+ * @param {String} separator - The separator to use between flattened nodes.
161
+ *
162
+ * @return {Object}
163
+ */
164
+ export function flattenObject(object, separator = '.') {
165
+ const finalObject = {}
166
+ Object.entries(object).forEach(item => {
167
+ if (typeof item[1] === 'object') {
168
+ const flatObject = flattenObject(item[1])
169
+ Object.entries(flatObject).forEach(node => {
170
+ finalObject[item[0] + separator + node[0]] = node[1]
171
+ })
172
+ } else {
173
+ finalObject[item[0]] = item[1]
174
+ }
175
+ })
176
+ return finalObject
177
+ }
178
+
179
+ /**
180
+ * Returns whether a value is thenable.
181
+ * @ignore
182
+ *
183
+ * @param {} value - The value to test.
184
+ *
185
+ * @return {Boolean}
186
+ */
187
+ export function isThenable(value) {
188
+ return (
189
+ Boolean(value) &&
190
+ (typeof value === 'object' || typeof value === 'function') &&
191
+ typeof value.then === 'function'
192
+ )
193
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Default values for all Dinero objects.
3
+ *
4
+ * You can override default values for all subsequent Dinero objects by changing them directly on the global `Dinero` object.
5
+ * Existing instances won't be affected.
6
+ *
7
+ * @property {Number} defaultAmount - The default amount for new Dinero objects (see {@link module:Dinero Dinero} for format).
8
+ * @property {String} defaultCurrency - The default currency for new Dinero objects (see {@link module:Dinero Dinero} for format).
9
+ * @property {Number} defaultPrecision - The default precision for new Dinero objects (see {@link module:Dinero Dinero} for format).
10
+ *
11
+ * @example
12
+ * // Will set currency to 'EUR' for all Dinero objects.
13
+ * Dinero.defaultCurrency = 'EUR'
14
+ *
15
+ * @type {Object}
16
+ */
17
+ export const Defaults = {
18
+ defaultAmount: 0,
19
+ defaultCurrency: 'USD',
20
+ defaultPrecision: 2
21
+ }
22
+
23
+ /**
24
+ * Global settings for all Dinero objects.
25
+ *
26
+ * You can override global values for all subsequent Dinero objects by changing them directly on the global `Dinero` object.
27
+ * Existing instances won't be affected.
28
+ *
29
+ * @property {String} globalLocale - The global locale for new Dinero objects (see {@link module:Dinero~setLocale setLocale} for format).
30
+ * @property {String} globalFormat - The global format for new Dinero objects (see {@link module:Dinero~toFormat toFormat} for format).
31
+ * @property {String} globalRoundingMode - The global rounding mode for new Dinero objects (see {@link module:Dinero~multiply multiply} or {@link module:Dinero~divide divide} for format).
32
+ * @property {String} globalFormatRoundingMode - The global rounding mode to format new Dinero objects (see {@link module:Dinero~toFormat toFormat} or {@link module:Dinero~toRoundedUnit toRoundedUnit} for format).
33
+ * @property {(String|Promise)} globalExchangeRatesApi.endpoint - The global exchange rate API endpoint for new Dinero objects, or the global promise that resolves to the exchanges rates (see {@link module:Dinero~convert convert} for format).
34
+ * @property {String} globalExchangeRatesApi.propertyPath - The global exchange rate API property path for new Dinero objects (see {@link module:Dinero~convert convert} for format).
35
+ * @property {Object} globalExchangeRatesApi.headers - The global exchange rate API headers for new Dinero objects (see {@link module:Dinero~convert convert} for format).
36
+ *
37
+ * @example
38
+ * // Will set locale to 'fr-FR' for all Dinero objects.
39
+ * Dinero.globalLocale = 'fr-FR'
40
+ * @example
41
+ * // Will set global exchange rate API parameters for all Dinero objects.
42
+ * Dinero.globalExchangeRatesApi = {
43
+ * endpoint: 'https://yourexchangerates.api/latest?base={{from}}',
44
+ * propertyPath: 'data.rates.{{to}}',
45
+ * headers: {
46
+ * 'user-key': 'xxxxxxxxx'
47
+ * }
48
+ * }
49
+ *
50
+ * @type {Object}
51
+ */
52
+ export const Globals = {
53
+ globalLocale: 'en-US',
54
+ globalFormat: '$0,0.00',
55
+ globalRoundingMode: 'HALF_EVEN',
56
+ globalFormatRoundingMode: 'HALF_AWAY_FROM_ZERO',
57
+ globalExchangeRatesApi: {
58
+ endpoint: undefined,
59
+ headers: undefined,
60
+ propertyPath: undefined
61
+ }
62
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Static methods for Dinero.
3
+ * @ignore
4
+ *
5
+ * @type {Object}
6
+ */
7
+ export default {
8
+ /**
9
+ * Returns an array of Dinero objects, normalized to the same precision (the highest).
10
+ *
11
+ * @memberof module:Dinero
12
+ * @method
13
+ *
14
+ * @param {Dinero[]} objects - An array of Dinero objects
15
+ *
16
+ * @example
17
+ * // returns an array of Dinero objects
18
+ * // both with a precision of 3
19
+ * // and an amount of 1000
20
+ * Dinero.normalizePrecision([
21
+ * Dinero({ amount: 100, precision: 2 }),
22
+ * Dinero({ amount: 1000, precision: 3 })
23
+ * ])
24
+ *
25
+ * @return {Dinero[]}
26
+ */
27
+ normalizePrecision(objects) {
28
+ const highestPrecision = objects.reduce((a, b) =>
29
+ Math.max(a.getPrecision(), b.getPrecision())
30
+ )
31
+ return objects.map(object =>
32
+ object.getPrecision() !== highestPrecision
33
+ ? object.convertPrecision(highestPrecision)
34
+ : object
35
+ )
36
+ },
37
+ /**
38
+ * Returns the smallest Dinero object from an array of Dinero objects
39
+ *
40
+ * @memberof module:Dinero
41
+ * @method
42
+ *
43
+ * @param {Dinero[]} objects - An array of Dinero objects
44
+ *
45
+ * @example
46
+ * // returns the smallest Dinero object with amount of 500 from an array of Dinero objects with different precisions
47
+ * Dinero.minimum([
48
+ * Dinero({ amount: 500, precision: 3 }),
49
+ * Dinero({ amount: 100, precision: 2 })
50
+ * ])
51
+ * @example
52
+ * // returns the smallest Dinero object with amount of 50 from an array of Dinero objects
53
+ * Dinero.minimum([
54
+ * Dinero({ amount: 50 }),
55
+ * Dinero({ amount: 100 })
56
+ * ])
57
+ *
58
+ * @return {Dinero[]}
59
+ */
60
+ minimum(objects) {
61
+ const [firstObject, ...tailObjects] = objects
62
+ let currentMinimum = firstObject
63
+
64
+ tailObjects.forEach(obj => {
65
+ currentMinimum = currentMinimum.lessThan(obj) ? currentMinimum : obj
66
+ })
67
+
68
+ return currentMinimum
69
+ },
70
+ /**
71
+ * Returns the biggest Dinero object from an array of Dinero objects
72
+ *
73
+ * @memberof module:Dinero
74
+ * @method
75
+ *
76
+ * @param {Dinero[]} objects - An array of Dinero objects
77
+ *
78
+ * @example
79
+ * // returns the biggest Dinero object with amount of 20, from an array of Dinero objects with different precisions
80
+ * Dinero.maximum([
81
+ * Dinero({ amount: 20, precision: 2 }),
82
+ * Dinero({ amount: 150, precision: 3 })
83
+ * ])
84
+ * @example
85
+ * // returns the biggest Dinero object with amount of 100, from an array of Dinero objects
86
+ * Dinero.maximum([
87
+ * Dinero({ amount: 100 }),
88
+ * Dinero({ amount: 50 })
89
+ * ])
90
+ *
91
+ * @return {Dinero[]}
92
+ */
93
+ maximum(objects) {
94
+ const [firstObject, ...tailObjects] = objects
95
+ let currentMaximum = firstObject
96
+
97
+ tailObjects.forEach(obj => {
98
+ currentMaximum = currentMaximum.greaterThan(obj) ? currentMaximum : obj
99
+ })
100
+
101
+ return currentMaximum
102
+ }
103
+ }