ts-math-utils
Version:
Math-based objects not included in JS, built in TS.
617 lines (519 loc) • 23.2 kB
text/typescript
import { Interval, IntervalNumber } from '../src/interval';
describe('Interval', () => {
it('should create a valid interval from IntervalNumber objects', () => {
const interval = new Interval({
a: new IntervalNumber(1, true),
b: new IntervalNumber(5, false),
});
expect(interval.toString()).toBe('[1, 5)');
});
it('should create a valid interval from a string', () => {
const iInterval = Interval.toInterval('(1, 5]');
const interval = new Interval(iInterval);
expect(interval.toString()).toBe('(1, 5]');
});
it('should create a valid interval of bigint from a string', () => {
const iInterval = Interval.toInterval('(1n, 5n]');
const interval = new Interval(iInterval);
expect(interval.toString()).toBe('(1n, 5n]');
});
it('should check if an IntervalNumber is contained within the interval', () => {
const interval = new Interval({
a: new IntervalNumber(1, true),
b: new IntervalNumber(5, false),
});
expect(interval.contains(new IntervalNumber(3))).toBe(true);
expect(interval.contains(new IntervalNumber(5))).toBe(false);
expect(interval.contains(new IntervalNumber(1))).toBe(true);
expect(interval.contains(new IntervalNumber(0))).toBe(false);
expect(interval.contains(new IntervalNumber(5, false))).toBe(false);
});
it('should check if another interval is contained within the interval', () => {
const interval = new Interval({
a: new IntervalNumber(1, true),
b: new IntervalNumber(10, false),
});
const containedInterval = new Interval({
a: new IntervalNumber(3, true),
b: new IntervalNumber(7, true),
});
const sameInterval = new Interval({
a: new IntervalNumber(1, true),
b: new IntervalNumber(10, false),
});
const sameBackweardInterval = new Interval({
a: new IntervalNumber(10, false),
b: new IntervalNumber(1, true),
});
const flipfloppedInterval = new Interval({
a: new IntervalNumber(1, false),
b: new IntervalNumber(10, true),
});
expect(interval.contains(containedInterval)).toBe(true);
expect(interval.contains(sameInterval)).toBe(true);
expect(containedInterval.contains(interval)).toBe(false);
expect(interval.contains(sameBackweardInterval)).toBe(true);
expect(interval.contains(flipfloppedInterval)).toBe(false);
expect(flipfloppedInterval.contains(interval)).toBe(false);
});
it('should correctly check containsMin for open and closed boundaries', () => {
const interval = new Interval({
a: new IntervalNumber(1, true),
b: new IntervalNumber(5, false),
});
// Closed min
expect(interval.containsMin(new IntervalNumber(1, true))).toBe(true);
// Open min
expect(interval.containsMin(new IntervalNumber(1, false))).toBe(true);
// Value inside
expect(interval.containsMin(new IntervalNumber(2, true))).toBe(true);
// Value below min
expect(interval.containsMin(new IntervalNumber(0, true))).toBe(false);
// Value at max
expect(interval.containsMin(new IntervalNumber(5, true))).toBe(false);
// Value at max open
expect(interval.containsMin(new IntervalNumber(5, false))).toBe(false);
});
it('should correctly check containsMax for open and closed boundaries', () => {
const interval = new Interval({
a: new IntervalNumber(1, true),
b: new IntervalNumber(5, false),
});
// Closed max
expect(interval.containsMax(new IntervalNumber(5, true))).toBe(false);
// Open max
expect(interval.containsMax(new IntervalNumber(5, false))).toBe(true);
// Value inside
expect(interval.containsMax(new IntervalNumber(4, true))).toBe(true);
// Value above max
expect(interval.containsMax(new IntervalNumber(6, true))).toBe(false);
// Value at min
expect(interval.containsMax(new IntervalNumber(1, true))).toBe(true);
// Value at min open
expect(interval.containsMax(new IntervalNumber(1, false))).toBe(false);
});
it('should check if two intervals overlap', () => {
const interval1 = new Interval({
a: new IntervalNumber(1, true),
b: new IntervalNumber(5, false),
});
const interval2 = new Interval({
a: new IntervalNumber(4, true),
b: new IntervalNumber(10, true),
});
expect(interval1.overlaps(interval2)).toBe(true);
expect(interval2.overlaps(interval1)).toBe(true);
});
it('should validate a correct interval string', () => {
expect(Interval.validIntervalString('[1, 5)')).toBe(true);
expect(Interval.validIntervalString('(1, 5]')).toBe(true);
expect(Interval.validIntervalString('[1, 5]')).toBe(true);
expect(Interval.validIntervalString('(1, 5)')).toBe(true);
expect(Interval.validIntervalString('[5, 1)')).toBe(true);
expect(Interval.validIntervalString('(5, 1]')).toBe(true);
expect(Interval.validIntervalString('[5, 1]')).toBe(true);
expect(Interval.validIntervalString('(5, 1)')).toBe(true);
expect(Interval.validIntervalString('[-5, 0)')).toBe(true);
expect(Interval.validIntervalString('(-5, 0]')).toBe(true);
expect(Interval.validIntervalString('[-5, 0]')).toBe(true);
expect(Interval.validIntervalString('(-5, 0)')).toBe(true);
expect(Interval.validIntervalString('[-Infinity, Infinity)')).toBe(true);
expect(Interval.validIntervalString('(-Infinity, Infinity]')).toBe(true);
expect(Interval.validIntervalString('[-Infinity, Infinity]')).toBe(true);
expect(Interval.validIntervalString('(-Infinity, Infinity)')).toBe(true);
expect(Interval.validIntervalString('[Infinity, -Infinity)')).toBe(true);
expect(Interval.validIntervalString('[Infinity, -Infinity]')).toBe(true);
expect(Interval.validIntervalString('(Infinity, -Infinity]')).toBe(true);
expect(Interval.validIntervalString('(Infinity, -Infinity)')).toBe(true);
});
it('should correctly check containsNumber for open and closed boundaries', () => {
const closedInterval = new Interval({
a: new IntervalNumber(1, true),
b: new IntervalNumber(5, true),
});
const openInterval = new Interval({
a: new IntervalNumber(1, false),
b: new IntervalNumber(5, false),
});
// Closed interval: [1, 5]
expect(closedInterval.containsNumber(1)).toBe(true);
expect(closedInterval.containsNumber(5)).toBe(true);
expect(closedInterval.containsNumber(3)).toBe(true);
expect(closedInterval.containsNumber(0)).toBe(false);
expect(closedInterval.containsNumber(6)).toBe(false);
// Open interval: (1, 5)
expect(openInterval.containsNumber(1)).toBe(false);
expect(openInterval.containsNumber(5)).toBe(false);
expect(openInterval.containsNumber(3)).toBe(true);
expect(openInterval.containsNumber(0)).toBe(false);
expect(openInterval.containsNumber(6)).toBe(false);
});
it('should throw an error for invalid intervals with equal endpoints that are both excluded', () => {
expect(() => {
new Interval({
a: new IntervalNumber(5, false),
b: new IntervalNumber(5, false),
});
}).toThrow(
'Invalid interval: Cannot exclude either minimum (5) or maximum (5) values if they are equal.'
);
});
it('should handle intervals with Infinity and -Infinity', () => {
const interval = new Interval({
a: new IntervalNumber(-Infinity, false),
b: new IntervalNumber(Infinity, false),
});
expect(interval.toString()).toBe('(-Infinity, Infinity)');
expect(interval.contains(new IntervalNumber(0))).toBe(true);
});
it('should invalidate an incorrect interval string', () => {
expect(Interval.validIntervalString('1, 5)')).toBe(false);
expect(Interval.validIntervalString('(1, 5')).toBe(false);
});
it('should invalidate interval strings with missing brackets or invalid characters', () => {
expect(Interval.validIntervalString('1, 5)')).toBe(false);
expect(Interval.validIntervalString('(1, 5')).toBe(false);
expect(Interval.validIntervalString('[1, 5')).toBe(false);
expect(Interval.validIntervalString('(1, 5]abc')).toBe(false);
expect(Interval.validIntervalString('abc[1, 5]')).toBe(false);
expect(Interval.validIntervalString('1, 5')).toBe(false);
});
it('should throw an error for an invalid interval string in toInterval', () => {
expect(() => {
Interval.toInterval('1, 5)');
}).toThrow('Invalid interval string: 1, 5)');
});
it('should correctly update the min and max values', () => {
const interval = new Interval({
a: new IntervalNumber(1, true),
b: new IntervalNumber(10, true),
});
interval.min = new IntervalNumber(0, false);
interval.max = new IntervalNumber(15, false);
expect(interval.toString()).toBe('(0, 15)');
});
it('should handle intervals with equal endpoints that are both included', () => {
const interval = new Interval({
a: new IntervalNumber(5, true),
b: new IntervalNumber(5, true),
});
expect(interval.toString()).toBe('[5, 5]');
expect(interval.contains(new IntervalNumber(5))).toBe(true);
});
it('should check if an interval contains its own endpoints', () => {
const interval = new Interval({
a: new IntervalNumber(1, true),
b: new IntervalNumber(5, false),
});
expect(interval.contains(new IntervalNumber(1))).toBe(true);
expect(interval.contains(new IntervalNumber(5))).toBe(false);
});
it('should handle intervals with one endpoint as Infinity', () => {
const interval = new Interval({
a: new IntervalNumber(5, true),
b: new IntervalNumber(Infinity, false),
});
expect(interval.toString()).toBe('[5, Infinity)');
expect(interval.contains(new IntervalNumber(100))).toBe(true);
expect(interval.contains(new IntervalNumber(Infinity))).toBe(false);
});
it('should fail for intervals with equal endpoints where one is closed and the other is open', () => {
expect(() => {
const interval1 = new Interval({
a: new IntervalNumber(5, true),
b: new IntervalNumber(5, false),
});
}).toThrow('Invalid interval: Cannot exclude either minimum (5) or maximum (5) values if they are equal.');
expect(() => {
const interval2 = new Interval({
a: new IntervalNumber(5, false),
b: new IntervalNumber(5, true),
});
}).toThrow('Invalid interval: Cannot exclude either minimum (5) or maximum (5) values if they are equal.');
});
it('should correctly identify overlapping intervals with shared boundaries', () => {
const interval1 = new Interval({
a: new IntervalNumber(1, true),
b: new IntervalNumber(5, false),
});
const interval2 = new Interval({
a: new IntervalNumber(5, true),
b: new IntervalNumber(10, true),
});
expect(interval1.overlaps(interval2)).toBe(false);
expect(interval2.overlaps(interval1)).toBe(false);
});
it('should handle intervals with reversed endpoints', () => {
const interval = new Interval({
a: new IntervalNumber(10, true),
b: new IntervalNumber(1, false),
});
expect(interval.min.number).toBe(1);
expect(interval.max.number).toBe(10);
expect(interval.toString()).toBe('[10, 1)');
});
it('should handle intervals that include or exclude zero', () => {
const interval = new Interval({
a: new IntervalNumber(-5, true),
b: new IntervalNumber(0, false),
});
expect(interval.contains(new IntervalNumber(0))).toBe(false);
expect(interval.contains(new IntervalNumber(-5))).toBe(true);
});
it('should handle intervals with very large numbers', () => {
const interval = new Interval({
a: new IntervalNumber(Number.MIN_SAFE_INTEGER, true),
b: new IntervalNumber(Number.MAX_SAFE_INTEGER, false),
});
expect(interval.contains(new IntervalNumber(0))).toBe(true);
expect(interval.contains(new IntervalNumber(Number.MAX_SAFE_INTEGER))).toBe(false);
});
it('should correctly identify nested intervals', () => {
const outerInterval = new Interval({
a: new IntervalNumber(1, true),
b: new IntervalNumber(10, true),
});
const innerInterval = new Interval({
a: new IntervalNumber(3, true),
b: new IntervalNumber(7, true),
});
expect(outerInterval.contains(innerInterval)).toBe(true);
expect(innerInterval.contains(outerInterval)).toBe(false);
});
it('should correctly identify adjacent intervals', () => {
const interval1 = new Interval({
a: new IntervalNumber(1, true),
b: new IntervalNumber(5, false),
});
const interval2 = new Interval({
a: new IntervalNumber(5, true),
b: new IntervalNumber(10, true),
});
expect(interval1.overlaps(interval2)).toBe(false);
expect(interval2.overlaps(interval1)).toBe(false);
});
it('should handle intervals with mixed open and closed boundaries', () => {
const interval = new Interval({
a: new IntervalNumber(1, true),
b: new IntervalNumber(5, false),
});
expect(interval.contains(new IntervalNumber(1))).toBe(true);
expect(interval.contains(new IntervalNumber(5))).toBe(false);
});
it('should return true for equal IntervalNumbers (same number and isClosed)', () => {
const n1 = new IntervalNumber(5, true);
const n2 = new IntervalNumber(5, true);
expect(n1.equals(n2)).toBe(true);
});
it('should return false for IntervalNumbers with different numbers', () => {
const n1 = new IntervalNumber(5, true);
const n2 = new IntervalNumber(6, true);
expect(n1.equals(n2)).toBe(false);
});
it('should return false for IntervalNumbers with different isClosed', () => {
const n1 = new IntervalNumber(5, true);
const n2 = new IntervalNumber(5, false);
expect(n1.equals(n2)).toBe(false);
});
it('should return true when comparing to a number with same value and default isClosed', () => {
const n1 = new IntervalNumber(5, true);
expect(n1.equals(5)).toBe(true);
});
it('should return false when comparing to a number with different value', () => {
const n1 = new IntervalNumber(5, true);
expect(n1.equals(6)).toBe(false);
});
it('should throw an error for invalid interval strings with equal excluded endpoints', () => {
expect(() => {
new Interval('(5, 5)');
}).toThrow(
'Invalid interval string: (5, 5)'
);
});
it('should throw an error for invalid interval strings with equal excluded negative endpoints', () => {
expect(() => {
new Interval('(-3, -3)');
}).toThrow(
'Invalid interval string: (-3, -3)'
);
});
it('should throw an error for invalid interval strings with equal excluded infinite endpoints', () => {
expect(() => {
new Interval('(-Infinity, -Infinity)');
}).toThrow(
'Invalid interval string: (-Infinity, -Infinity)'
);
});
it('should throw an error when setting a to match b with both endpoints open', () => {
const interval = new Interval({
a: new IntervalNumber(1, true),
b: new IntervalNumber(5, false),
});
expect(() => {
interval.a = new IntervalNumber(5, false);
}).toThrow('Invalid interval. Cannot exclude either minimum and maximum values if they are equal.');
});
it('should not throw when setting a to match b if at least one endpoint is closed', () => {
const interval = new Interval({
a: new IntervalNumber(1, true),
b: new IntervalNumber(5, false),
});
expect(() => {
interval.a = new IntervalNumber(5, true);
}).not.toThrow();
expect(() => {
interval.b = new IntervalNumber(5, true);
interval.a = new IntervalNumber(5, false);
}).not.toThrow();
expect(() => {
interval.b = new IntervalNumber(5, false);
}).toThrow('Invalid interval. Cannot exclude either minimum and maximum values if they are equal.');
});
it('should throw an error when setting b to match a with both endpoints open', () => {
const interval = new Interval({
a: new IntervalNumber(5, false),
b: new IntervalNumber(10, true),
});
expect(() => {
interval.b = new IntervalNumber(5, false);
}).toThrow('Invalid interval. Cannot exclude either minimum and maximum values if they are equal.');
});
it('should not throw when setting b to match a if at least one endpoint is closed', () => {
const interval = new Interval({
a: new IntervalNumber(5, false),
b: new IntervalNumber(10, true),
});
expect(() => {
interval.b = new IntervalNumber(5, true);
}).not.toThrow();
expect(() => {
interval.b = new IntervalNumber(5, false);
interval.a = new IntervalNumber(5, true);
}).toThrow('Invalid interval. Cannot exclude either minimum and maximum values if they are equal.');
});
it('should update the min endpoint when setting min', () => {
const interval = new Interval({
a: new IntervalNumber(1, true),
b: new IntervalNumber(10, true),
});
interval.min = new IntervalNumber(0, false);
expect(interval.a.number).toBe(0);
expect(interval.a.isClosed).toBe(false);
expect(interval.min.number).toBe(0);
expect(interval.toString()).toBe('(0, 10]');
});
it('should update the min endpoint when endpoints are reversed', () => {
const interval = new Interval({
a: new IntervalNumber(10, true),
b: new IntervalNumber(1, false),
});
interval.min = new IntervalNumber(0, false);
// b is the min, so b should be updated
expect(interval.b.number).toBe(0);
expect(interval.b.isClosed).toBe(false);
expect(interval.min.number).toBe(0);
expect(interval.toString()).toBe('[10, 0)');
});
it('should throw an error when setting min to match the other endpoint with both open', () => {
const interval = new Interval({
a: new IntervalNumber(1, false),
b: new IntervalNumber(5, false),
});
expect(() => {
interval.min = new IntervalNumber(5, false);
}).toThrow('Invalid interval. Cannot exclude either minimum and maximum values if they are equal.');
});
it('should update the max endpoint when setting max', () => {
const interval = new Interval({
a: new IntervalNumber(1, true),
b: new IntervalNumber(10, true),
});
interval.max = new IntervalNumber(15, false);
expect(interval.b.number).toBe(15);
expect(interval.b.isClosed).toBe(false);
expect(interval.max.number).toBe(15);
expect(interval.toString()).toBe('[1, 15)');
});
it('should update the max endpoint when endpoints are reversed', () => {
const interval = new Interval({
a: new IntervalNumber(10, true),
b: new IntervalNumber(1, false),
});
interval.max = new IntervalNumber(15, false);
// a is the max, so a should be updated
expect(interval.a.number).toBe(15);
expect(interval.a.isClosed).toBe(false);
expect(interval.max.number).toBe(15);
expect(interval.toString()).toBe('(15, 1)');
});
it('should throw an error when setting max to match the other endpoint with both open', () => {
const interval = new Interval({
a: new IntervalNumber(1, false),
b: new IntervalNumber(5, false),
});
expect(() => {
interval.max = new IntervalNumber(1, false);
}).toThrow('Invalid interval. Cannot exclude either minimum and maximum values if they are equal.');
});
it('should return true for a valid open/closed interval', () => {
expect(Interval.validIntervalString('[1, 5)')).toBe(true);
expect(Interval.validIntervalString('(1, 5]')).toBe(true);
expect(Interval.validIntervalString('[1, 5]')).toBe(true);
expect(Interval.validIntervalString('(1, 5)')).toBe(true);
});
it('should return true for intervals with infinite endpoints', () => {
expect(Interval.validIntervalString('(-Infinity, 5]')).toBe(true);
expect(Interval.validIntervalString('[1, Infinity)')).toBe(true);
expect(Interval.validIntervalString('(-Infinity, Infinity)')).toBe(true);
});
it('should return true for equal endpoints if both are closed', () => {
expect(Interval.validIntervalString('[5, 5]')).toBe(true);
expect(Interval.validIntervalString('[0, 0]')).toBe(true);
expect(Interval.validIntervalString('[-Infinity, -Infinity]')).toBe(true);
expect(Interval.validIntervalString('[Infinity, Infinity]')).toBe(true);
expect(Interval.validIntervalString('[-5, -5]')).toBe(true);
});
it('should return false for equal endpoints if both open', () => {
expect(Interval.validIntervalString('(5, 5]')).toBe(false);
expect(Interval.validIntervalString('[5, 5)')).toBe(false);
expect(Interval.validIntervalString('(5, 5)')).toBe(false);
expect(Interval.validIntervalString('(-Infinity, -Infinity)')).toBe(false);
expect(Interval.validIntervalString('(-Infinity, -Infinity]')).toBe(false);
expect(Interval.validIntervalString('(-Infinity, -Infinity)')).toBe(false);
expect(Interval.validIntervalString('(-5, -5)')).toBe(false);
expect(Interval.validIntervalString('(-5, -5]')).toBe(false);
});
it('should return false for malformed or missing brackets', () => {
expect(Interval.validIntervalString('1, 5]')).toBe(false);
expect(Interval.validIntervalString('[1, 5')).toBe(false);
expect(Interval.validIntervalString('1, 5')).toBe(false);
expect(Interval.validIntervalString('')).toBe(false);
});
it('should return false for non-numeric endpoints', () => {
expect(Interval.validIntervalString('[a, b]')).toBe(false);
expect(Interval.validIntervalString('[1, b]')).toBe(false);
expect(Interval.validIntervalString('[a, 5]')).toBe(false);
});
it('should accept a number and convert it to IntervalNumber when setting a', () => {
const interval = new Interval({
a: new IntervalNumber(1, true),
b: new IntervalNumber(5, true),
});
interval.a = 3; // should use isClosed = true by default
expect(interval.a.number).toBe(3);
expect(interval.a.isClosed).toBe(true);
expect(interval.toString()).toBe('[3, 5]');
expect(interval.min.number).toBe(3);
});
it('should accept an IntervalNumber when setting a', () => {
const interval = new Interval({
a: new IntervalNumber(1, true),
b: new IntervalNumber(5, true),
});
interval.a = new IntervalNumber(2, false);
expect(interval.a.number).toBe(2);
expect(interval.a.isClosed).toBe(false);
expect(interval.toString()).toBe('(2, 5]');
expect(interval.min.number).toBe(2);
});
});