@eclipse-scout/core
Version:
Eclipse Scout runtime
240 lines (222 loc) • 7.65 kB
text/typescript
/*
* Copyright (c) 2010, 2024 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
import {objects, RoundingMode, strings} from '../index';
export const numbers = {
/**
* Converts the given decimal number to base-62 (i.e. the same value, but represented by [a-zA-Z0-9] instead of only [0-9]).
*/
toBase62(number: number): string {
if (number === undefined) {
return undefined;
}
let symbols = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'.split('');
let base = 62;
let s = '';
let n;
while (number >= 1) {
n = Math.floor(number / base);
s = symbols[(number - (base * n))] + s;
number = n;
}
return s;
},
/**
* Returns a random sequence of characters out of the set [a-zA-Z0-9] with the given length.
* @param length The default length is 8.
*/
randomId(length?: number): string {
length = length || 8;
let alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += alphabet[Math.floor(Math.random() * alphabet.length)];
}
return result;
},
/**
* Returns a random integer between 0 (inclusive) and the given `size` (exclusive).
*
* @param size The default size is {@link Number.MAX_SAFE_INTEGER}.
*/
randomInt(size?: number): number {
size = size || Number.MAX_SAFE_INTEGER;
return Math.floor(Math.random() * size);
},
/** @internal */
_correlationCounter: 1,
/**
* Generates a random ID suitable for use as correlation ID.
*
* Example:
*
* Hq5JY2kz3n/27
*
* The ID is generated from two different alphabets: 1. only letter, 2. only digits. By
* always selecting a random digit after two random characters, accidental "rude words"
* can be prevented.
*
* The characters[01olOL] are not used at all because they are easily confused.
*
* For a length of 11 (default), this method can theoretically generate over 200 trillion
* different IDs:
*
* 46^7 * 8^3 = 223'138'640'494'592
*
* To further reduce the risk of collisions, a monotonically increasing counter is added
* at the end of the result string (separated by "/").
*/
correlationId(length?: number): string {
length = length || 11;
let letters = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ';
let digits = '23456789';
let result = '';
for (let i = 0; i < length; i++) {
if ((i + 1) % 3 === 0) {
result += digits[Math.floor(Math.random() * digits.length)];
} else {
result += letters[Math.floor(Math.random() * letters.length)];
}
}
return result + '/' + (numbers._correlationCounter++);
},
/**
* Rounds a number to the given number of fraction digits.
*
* Numbers should not be rounded with the built-in Number.toFixed() method, since it
* behaves differently on different browsers. However, it is safe to apply toFixed()
* to the result of this method to ensure a fixed number of fraction digits (filled up
* with 0's) because this operation does not involve any rounding anymore.
* <p>
* If fractionDigits is omitted, the number will be rounded to integer by default.
* Rounding mode {@link RoundingMode.HALF_UP} is used as default.
*/
round(number: number, roundingMode?: RoundingMode, fractionDigits?: number): number {
if (number === null || number === undefined) {
return number;
}
fractionDigits = fractionDigits || 0;
// Do _not_ multiply with powers of 10 here, because that might cause rounding errors!
// Example: 1.005 with 2 fraction digits would result in 100.49999999999999
number = numbers.shiftDecimalPoint(number, fractionDigits);
switch (roundingMode) {
case RoundingMode.UP:
if (number < 0) {
number = -Math.ceil(Math.abs(number));
} else {
number = Math.ceil(number);
}
break;
case RoundingMode.DOWN:
if (number < 0) {
number = -Math.floor(Math.abs(number));
} else {
number = Math.floor(number);
}
break;
case RoundingMode.CEILING:
number = Math.ceil(number);
break;
case RoundingMode.FLOOR:
number = Math.floor(number);
break;
case RoundingMode.HALF_DOWN:
if (number < 0) {
number = Math.round(number);
} else {
number = -Math.round(-number);
}
break;
// case RoundingMode.HALF_EVEN:
// case RoundingMode.UNNECESSARY:
// not implemented, default is used.
default:
// RoundingMode.HALF_UP is used as default
if (number < 0) {
number = -Math.round(Math.abs(number));
} else {
number = Math.round(number);
}
}
return numbers.shiftDecimalPoint(number, -fractionDigits);
},
/**
* Shifts the decimal point in the given number by a certain distance. While the result is also
* number, the method uses string operations to move the decimal point. This prevents rounding
* errors as long as the number does not exceed JavaScript's Number precision.
*
* The argument 'move' describes the distance how far the decimal point should be moved:
* 0 = do no move (1.57 --> 1.57)
* > 0 = move to right (1.57 --> 15.7)
* < 0 = move to left (1.57 --> 0.157)
*/
shiftDecimalPoint(number: number, move: number): number {
if (number === null || number === undefined || !move) {
return number;
}
let sign = (number ? (number < 0 ? -1 : 1) : 0);
let distance = Math.abs(move);
number = Math.abs(number);
let s = strings.asString(number);
if (s.indexOf('e') !== -1) {
s = number.toFixed(20);
}
let a;
if (move < 0) {
// move to left
s = strings.repeat('0', distance) + s;
a = s.split('.', 2);
if (a.length === 1) {
s = s.substr(0, s.length - distance) + '.' + s.substr(s.length - distance);
} else {
s = a[0].substr(0, a[0].length - distance) + '.' + a[0].substr(a[0].length - distance) + a[1];
}
} else if (move > 0) {
// move to right
s += strings.repeat('0', distance);
a = s.split('.', 2);
if (a.length === 2) {
s = a[0] + a[1].substr(0, distance) + '.' + a[1].substr(distance);
}
}
// Remove multiple leading zeros to prevent interpretation as octal number
s = s.replace(/^0*(\d)/g, '$1');
return Number(s) * sign;
},
/**
* Ensures that the given number is really a number.
* <p>
* If it already is a number, the number will be returned.
* Otherwise, a Number is created.
*
* @param number may be of type number or string.
*/
ensure(number: number | string): number {
if (objects.isNullOrUndefined(number)) {
return number;
}
return Number(number);
},
/**
* Returns true if the given number is of type number but not NaN.
*/
isNumber(number: any): number is number {
return typeof number === 'number' && !isNaN(number);
},
/**
* Returns true if the given number is an integer.
*/
isInteger(number: any): number is number {
return numbers.isNumber(number) && isFinite(number) && Math.floor(number) === number;
},
/** @internal */
_setCorrelationCounter(val: number) {
numbers._correlationCounter = val;
}
};