@tubular/math
This library provides several useful math utilities as well as pass-through declarations of the standard JavaScript Math library.
These include min-max seeking, zero finding, assistance for integer and modular math, angle calculations, conversions and formatting, and support for working with spherical coordinates.
The chief motivation behind the creation of this library was solving astronomical calculations (as those available via @tubular/astronomy), but the functionality provided here is applicable to many tasks.
Installation
Via npm
npm install @tubular/math
import { cos_deg, interpolate, union,
...} from '@tubular/math'; // ESM
...or...
const { cos_deg, interpolate, union,
...} = require('@tubular/math'); // CommonJS
Documentation examples will assume @tubular/math has been imported as above.
Via <script>
tag
To remotely download the full code as an ES module:
<script type="module">
import('https://unpkg.com/@tubular/math/dist/index.min.mjs').then(pkg => {
const { cos_deg, interpolate, union } = pkg;
// ...
});
</script>
For the old-fashioned UMD approach:
<script src="https://unpkg.com/@tubular/math/dist/index.min.js"></script>
The @tubular/math package will be made available via the global variable tbMath
. Functions, classes, and constants will also be available via this variable, such as tbMath.cos_deg
, tbMath.interpolate
, tbMath.union
, etc.
Simple pass-through functions and constants from Math
abs
, acos
, acosh
, asin
, asinh
, atan
, atan2
, atanh
, cbrt
, clz32
, cos
, cosh
, E
, exp
, expm1
, fround
, hypot
, imul
, LN10
, LN2
, log
, log10
, LOG10E
, log1p
, log2
, LOG2E
, max
, min
, PI
, pow
, sign
, sin
, sinh
, sqrt
, SQRT1_2
, SQRT2
, tan
, tanh
, trunc
Additional math functions
function acos_deg(x: number): number;
Same as Math.acos
, but returns a result in degrees rather than radians.
function acot(x: number): number;
Arccotangent (cot‑1).
function acot2(y: number, x: number): number;
Two-argument version of arccotangent (cot‑1), conceptually related to atan2
.
function acot_deg(x: number): number;
Arccotangent (cot‑1), but returns a result in degrees rather than radians.
function acot2_deg(y: number, x: number): number;
Two-argument version of arccotangent (cot‑1), conceptually related to atan2
, returning a result in degrees rather than radians.
function asin_deg(x: number): number;
Same as Math.asin
, but returns a result in degrees rather than radians.
function atan_deg(x: number): number;
Same as Math.atan
, but returns a result in degrees rather than radians.
function ceil(x: number, multiple = 1): number;
Same as Math.ceil
when the second argument is omitted (or equals 1), otherwise rounds x
upward to the nearest integer multiple of multiple
.
function convertFromRadians(angle: number, unit: Unit): number
Converts an angle expressed in radians to an angle expressed in unit
, where unit
is one of the following enumerated values:
enum Unit { RADIANS, DEGREES, ARC_MINUTES, ARC_SECONDS, HOURS, HOUR_ANGLE_MINUTES, HOUR_ANGLE_SECONDS, ROTATIONS, GRADS }
function convertToRadians(angle: number, unit: Unit): number
Converts an angle expressed in unit
to an angle expressed in radians, where unit
is one of the above-listed enumerated values, e.g. Unit.ARC_MINUTES
.
function cos_deg(x: number): number;
Same as Math.cos
, but accepts an argument in degrees rather than radians.
function div_tt0(x: number, y: number): number;
Integer division with “truncation toward zero”, that is, positive results of x_/y_ round downward to the nearest integer, and negative results round upward to the nearest integer.
function div_rd(x: number, y: number): number;
Integer division where the result of x_/y_ always rounds downward to the nearest integer.
function floor(x: number, multiple = 1): number;
Same as Math.floor
when the second argument is omitted (or equals 1), otherwise rounds x
downward to the nearest integer multiple of multiple
.
function interpolate(x0: number, x: number, x1: number, y0: number, y1: number): number;
Given two points, (x_0, _y0), (x_1, _y1), and a value x_ such that _x0 ≤ x_ ≤ _x1, this function returns a corresponding y value by simple linear interpolation.
function interpolateModular(x0: number, x: number, x1: number, y0: number, y1: number, modulus: number, signedResult = false): number;
This function works identically to the interpolate
function above except that results are pinned within a modular range of [0, modulus) when signedResult
is omitted or false
, or [‑modulus/2, modulus/2) if signedResult
is true.
This is useful for interpolating angular values, not only to pin results to a range such as [0, 360) or [‑180, 180), but when the input values might span across a modular discontinuity.
function interpolateTabular(xx: number[], yy: number[], x: number, maxSpan = 0): number;
This function finds the value of y_ for a given value of _x by tabular interpolation, using (x_, _y) pairs from xx
and yy
. maxSpan
can typically be omitted (or set to 0).
For special cases where there is a possible discontinuity in the source of the tabular values (e.g. some values come from an historical table of past values, whereas others come from a predicative formula for future values), a non-zero maxSpan
limits the range of tabular values used for the interpolation to tabular value pairs where the x value is in the range [x - maxSpan
, x + maxSpan
].
Note: Using a non-zero maxSpan
imposes the requirement that the xx
array be sorted in ascending order, and the yy
array be sorted so that its values pair correctly with the sorted xx
values.
What the maxSpan
limit achieves, in terms of the example cited, is the creation of three separate types of results: results that are based only on historical values, results which are based only on predicative values, and a transitional range of results where the interpolation smoothly blends the two source ranges using weighted averaging.
function irandom(maxValue: number): number;
function irandom(lowest: number, highest: number): number;
- With one argument, this function returns a random integer in the range [1, maxValue].
- With two arguments, this function returns a random integer in the range [lowest, highest].
function limitNeg1to1(x: number): number;
Returns the value of x_ pinned within the range [‑1, 1], a function particularly useful for preventing NaN
results from asin
or acos
when the value of _x might overflow this range slightly due to rounding errors.
function mod(x: number, y: number): number;
This is equivalent to x % y
except for the function’s behavior with negative argument values. The resulting value tracks the sign of y
, not the sign of x
. When y
is positive, the returned value will be in the range [0, y_). When y
is negative, the returned value will be in the range (y_, 0]. (This matches the behavior of the Python %
operator.)
function mod2(x: number, y: number): number;
Like the mod
function above, except when y
is positive, the returned value will be in the range [‑y_/2, _y/2). When y
is negative, the returned value will be in the range (‑y_/2, _y/2].
function random(): number;
function random(maxValue: number): number;
function random(lowest: number, highest: number): number;
- With no arguments, this function is equivalent to
Math.random
. - With one argument, this function returns a random number in the range [0, maxValue).
- With two arguments, this function returns a random number in the range [lowest, highest).
function round(x: number, multiple = 1): number;
Same as Math.round
when the second argument is omitted (or equals 1), otherwise rounds x
to the nearest integer multiple of multiple
, rounding upward when x is exactly halfway between two multiples.
function signZN(x: number): number;
Like Math.sign
, but returns -1 for an x
of 0.
function signZP(x: number): number;
Like Math.sign
, but returns 1 for an x
of 0.
function sin_deg(x: number): number;
Same as Math.sin
, but accepts an argument in degrees rather than radians.
function squared(x: number): number;
Returns x2.
function tan_deg(x: number): number;
Same as Math.tan
, but accepts an argument in degrees rather than radians.
function to_degree(x: number): number;
Converts x
in radians to degrees.
function to_radian(x: number): number;
Converts x
in degrees to radians.
function union(r1: Rectangle, r2: Rectangle): Rectangle;
Returns the smallest Rectangle
which encloses both r1
and r2
.
Data types
interface Point {
x: number;
y: number;
}
interface Point3D {
x: number;
y: number;
z: number;
}
interface Rectangle {
x: number;
y: number;
w: number;
h: number;
}
The Angle
class
The Angle
class represents immutable angle values, with methods to facilitate angular calculations and formatting angular values. Angle instances cache sin, cos, and tan values internally so they only need to be computed once.
Constructor
constructor(angle = 0, unit?: Unit, mode = Mode.RANGE_LIMIT_SIGNED)
- With no arguments,
new Angle()
creates a zero-valued angle, equivalent to the constant Angle.ZERO. - With one argument an
Angle
instance ofangle
radians is created. - With two arguments an
Angle
instance ofangle
expressed inunit
is created. As a radian value,angle
will be adjusted to the range [-π_, _π), or [-180, 180) as degrees, etc. - Given three arguments, and when
mode
isMode.RANGE_LIMIT_SIGNED
, a newAngle
instance is created as described above.
Amode
ofMode.RANGE_LIMIT_NONNEGATIVE
coercesangle
into the range [0, 2 π_) in radians, [0, 360) in degrees, etc.
Amode
of `Mode.RANGEUNLIMITEDleaves the value of
angle` as-is.
Static Angle
factory methods
Angle.asin
, Angle.asin_nonneg
, Angle.acos
, Angle.atan
, Angle.atan_nonneg
, Angle.atan2
, and Angle.atan2_nonneg
all function the same as the like-named general functions, but returning an Angle
instance equivalent to the expected value in radians.
Angle.parse(s: string, throwException = false): Angle
This method parses a string, either in degrees or hours, and an Angle instance. If the string can’t be parsed as a valid value, the method either returns null
, or if throwException
is specified and true
, throws an "Invalid angle" exception.
If the characters h
or m
, are found within the string, the angle is treated as an hour angle, otherwise the angle is considered to be in degrees. A leading -
will negate the angle value, or, when parsing degree values, a trailing e
or s
will negate the value. (These rules are case-insensitive.)
No range-limiting is applied — "700"
will parse as an angle of 700°, not 340° or -20°.
Enums
enum Unit { RADIANS, DEGREES, ARC_MINUTES, ARC_SECONDS,
HOURS, HOUR_ANGLE_MINUTES, HOUR_ANGLE_SECONDS,
ROTATIONS, GRADS }
enum Mode { RANGE_LIMIT_SIGNED, RANGE_LIMIT_NONNEGATIVE,
RANGE_UNLIMITED }
Accessors for angle values in different units, and a conversion function
arcMinutes
, arcSeconds
, degrees
, grads
, hours
, radians
, rotations
For example, given:
const a = new Angle(60, Unit.DEGREES);
...a.hours
has a value of 4, a.arcMinutes
is 3600, and a.rotations
is 0.16666666666666666.
You can also access different unit values via the function:
getAngle(unit = Unit.RADIANS): number
...such that a.getAngle(Unit.HOURS)
would return 4, a.getAngle(Unit.ARC_MINUTES)
would return 3600, etc.
Caching trigonometric accessors
cos
, sin
, tan
Given a
as defined in the previous examples, a.cos
returns 0.5 (well, okay, 0.5000000000000001 because rounding isn’t perfect). Note: No parenthesis!
Angle
methods which return instances of Angle
add(angle2: Angle, mode = Mode.RANGE_LIMIT_SIGNED): Angle;
subtract(angle2: Angle, mode = Mode.RANGE_LIMIT_SIGNED): Angle;
complement(mode = Mode.RANGE_LIMIT_SIGNED): Angle;
supplement(mode = Mode.RANGE_LIMIT_SIGNED): Angle;
opposite(mode = Mode.RANGE_LIMIT_SIGNED): Angle;
negate(mode = Mode.RANGE_LIMIT_SIGNED): Angle;
multiply(x: number, mode = Mode.RANGE_LIMIT_SIGNED): Angle;
divide(x: number, mode = Mode.RANGE_LIMIT_SIGNED): Angle;
Omitting the default mode
argument of Mode.RANGE_LIMIT_SIGNED
for clarity:
add(angle2: Angle): Angle;
subtract(angle2: Angle): Angle;
complement(): Angle;
supplement(): Angle;
opposite(): Angle;
negate(): Angle;
multiply(x: number): Angle;
divide(x: number): Angle;
For non-negative results, without the need to specify Mode.RANGE_LIMIT_NONNEGATIVE
:
add_nonneg(angle2: Angle): Angle;
subtract_nonneg(angle2: Angle): Angle;
complement_nonneg(): Angle;
supplement_nonneg(): Angle;
opposite_nonneg(): Angle;
negate_nonneg(): Angle; // Sounds contradictory perhaps, but `negate_nonneg` of 20° is simply 340°.
multiply_nonneg(x: number): Angle;
divide_nonneg(x: number): Angle;
Formatting/Stringifying
toString(): string
With no arguments, the default string conversion is to display an angle value in decimal degrees with three digits of precision, followed by a degree (°
) symbol.
toString(format?: number, precision?: number): string
Stringifies an angle as decimal degrees according to format
, specified using the following constants:
const FMT_DD = 0x01; // Integer degrees zero-padded to two digits
const FMT_HH = 0x01; // Integer hours zero-padded to two digits (for use with
// toHourString and toTimeString)
const FMT_DDD = 0x02; // Integer degrees zero-padded to three digits
const FMT_MINS = 0x04; // Display arcminutes
const FMT_SECS = 0x08; // Display arcseconds (and arcminutes too)
const FMT_SIGNED = 0x10; // Prefix positive values with `+` (`-` appears when needed)
These constants can be combined using |
to express combinations of formatting options, such as FMT_DD | FMT_MINS | FMT_SIGNED
to format an angle as a signed angle, integer portion zero-padded to two digits, with minute resolution, e.g. 3.5° becomes +03°30'
.
The optional precision
parameter (defaulting to 0) specifies how many decimal places to display as part of the smallest unit or subunit. For example:
new Angle(3.5, Unit.DEGREES).toString(null, 2)
➜ 3.50°
new Angle(3.5, Unit.DEGREES).toString(FMT_MINS)
➜ 3°30'
new Angle(3.5, Unit.DEGREES).toString(FMT_MINS, 2)
➜ 3°30.00'
toSuffixedString(positiveSuffix: string, negativeSuffix: string,
format?: number, precision?: number): string
This works like toString()
, but rather than indicating the sign of the angle value using a leading +
or -
sign, either positiveSuffix
or negativeSuffix
is added at the end of the string representation. Typical values of positiveSuffix
would be N
or W
for North or West. Typical values of negativeSuffix
would be S
or E
for South or East.
toHourString(format?: number, precision?: number): string
Formats an angle as an hour angle (24 hours = 360°), using h
, m
and s
to denote hours, minutes, and seconds, e.g. 12h34m56s
.
toTimeString(format?: number, precision?: number): string
Formats an angle as an hour angle (24 hours = 360°), using colons (:
) to separate hours, minutes, and seconds, e.g. 12:34:56
.
The SphericalPosition
class
Instances of this class are immutable objects containing two Angle
values, representing longitude and latitude (or right ascension and declination, or azimuth and altitude).
Constructor
constructor(longitude: Angle | number = 0, latitude: Angle | number = 0,
longUnit = Unit.RADIANS, latUnit?: Unit)
- With no arguments an instance equivalent to 0°N, 0°W is created.
- With two arguments an instance of
longitude
,latitude
is created. Numerical arguments will be interpreted as radians. - With three arguments an instance of
longitude
,latitude
is created, numerical arguments both interpreted according tolongUnit
. - With four arguments an instance of
longitude
,latitude
is created, numerical arguments interpreted according tolongUnit
andlatUnit
, respectively.
Accessors
altitude
, azimuth
, declination
, latitude
, longitude
, rightAscension
azimuth
and rightAscension
are equivalent to longitude
.
altitude
and declination
are equivalent to latitude
.
Method
distanceFrom(p: SphericalPosition): Angle
This method computes the angular distance between a SphericalPosition
instance and another SphericalPosition
instance.
The SphericalPosition3D
class
Instances of this class are a subclass of SphericalPosition
with the addition of a radius value, specifying a unique point in a 3D space.
Constructor
constructor(longitude?: Angle | number, latitude?: Angle | number,
radius = 0, longUnit?: Unit, latUnit?: Unit)
This works the same way as the SphericalPosition
constructor, with the addition of a radius
argument.
Static SphericalPosition3D
factory methods
SphericalPosition3D.convertRectangular(x: number, y: number, z: number): SphericalPosition3D
SphericalPosition3D.convertRectangular(point: Point3D): SphericalPosition3D
These methods convert rectangle coordinates to 3D spherical coordinates.
from2D(pos: SphericalPosition, radius: number): SphericalPosition3D
This method creates a SphericalPosition3D
instance from a SphericalPosition
instance and a radius
value.
Accessors
radius
: The radius value.
xyz
: The rectangular coordinates for the SphericalPosition3D
instance.
The MinMaxFinder
class
This class finds estimated minima or maxima of numerical functions by parabolic interpolation, using Brent’s Method.
Constructor
constructor(minMaxSeekingFunction: (x: number) => number,
tolerance: number, maxIterations: number,
xa: number, xb: number, xc: number)
This creates an instance of MinMaxFinder
to find the x_ value for which the value minMaxSeekingFunction(x)
reaches a minimum (or maximum) value over the range _xa ≤ x_ ≤ _xc, using the value x_b (also in the range _xa ≤ x_b ≤ _xc) as a hint for whether a minimum or maximum value should be sought.
Seeking is an iterative process. Once the difference in estimates between successive iterations is less than or equal to tolerance
an estimate with be returned.
Often very accurate estimates can be found in less than 10 iterations, but maxIterations
puts a limit on how many iterations will be attempted before possibly giving up, as some functions cannot be guaranteed to produce a solution.
Method
getXAtMinMax(): number
Returns the estimated value of x_ at the minimum (or maximum) point of the function minMaxSeekingFunction
in the range _xa ≤ x_ ≤ _xc within tolerance
of the best possible estimate, or the closest approximation reached when maxIterations
have been exhausted.
Accessors
Note: using any accessor will invoke getXAtMinMax()
if it has not already been invoked.
foundMaximum
true
if a maximum value rather than a minimum value was found. true
does not signify, however, that the result is a valid result found within tolerance
or maxIterations
.
foundMinimum
true
if a minimum value rather than a maximum value was found. true
does not signify, however, that the result is a valid result found within tolerance
or maxIterations
.
lastY
The last y value computed when either a solution was found or maxIterations
were exhausted.
iterationCount
The total number of iterations needed either to solve for the min/max value, or when maxIterations
was reached.
resolved
true
if a valid estimate was found within tolerance
before maxIterations
were exhausted.
The ZeroFinder
class
This class finds estimated zero-axis crossing/contact points of functions, i.e. x_ values at which _f(x) momentarily have a value of 0.
Constructors
constructor(zeroSeekingFunction: (x: number) => number, tolerance: number,
maxIterations: number, x1: number, x2: number, maxError?: number)
This creates an instance of ZeroFinder
to find the estimated x_ value at which the value zeroSeekingFunction(x)
becomes zero, over the range _x1 ≤ x_ ≤ _x2.
Like MinMaxFinder
, a solution is found using an iterative process. Once the difference in estimates between successive iterations is less than or equal to tolerance
an estimate with be returned.
If a maxError
value is provided, and the absolute value of the closest-to-zero result found after maxIterations
is greater than maxError
, getXAtZero()
will return NaN
.
constructor(zeroSeekingFunction: (x: number) => number, tolerance: number,
maxIterations: number, x1: number, y1: number, x2: number, y2: number, maxError?: number)
This constructor works the same as above except that the values y_1 and _y2 corresponding to x_1 and _x2 are provided precomputed.
Method
getXAtZero(): number
Returns the estimated value of x_ in the range _x1 ≤ x_ ≤ _x2 for which the function zeroSeekingFunction(x)
reaches 0, within tolerance
of the best possible estimate, or the closest approximation reached when maxIterations
have been exhausted.
The value of tolerance
applies not only to the difference between successive estimates of x, but to how close zeroSeekingFunction(x)
is to 0.
maxError
applies only to the closest-to-zero zeroSeekingFunction(x)
value found, resulting in getXAtZero()
returning NaN
if the absolute value of that closest value exceeds maxError
.
Accessors
Note: using any accessor will invoke getXAtZero()
if it has not already been invoked.
lastY
The last y value computed when either a solution was found or maxIterations
were exhausted.
iterationCount
The total number of iterations needed either to solve for the min/max value, or when maxIterations
was reached.
resolved
true
if a valid estimate was found within tolerance
before maxIterations
were exhausted.