Skip to content

Commit

Permalink
Added DurationSpec, Duration.of(DurationSpec), Instant.plus/.add(Dura…
Browse files Browse the repository at this point in the history
…tionSpec), Instant.minus(DurationSpec) and Instant.minus/add(Duration/DurationSpec)
  • Loading branch information
DavidDuwaer committed Nov 21, 2021
1 parent e9b4399 commit b330f93
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 59 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"scripts": {
"clear": "rimraf dist",
"build": "npm run clear && tsc",
"pretest": "npm build",
"build-dev": "npm run clear && tsc --sourceMap",
"test": "npm run build-dev && mocha -r ts-node/register tests/**/*.test.ts",
"dist": "npm install && npm run test && npm publish"
Expand Down
32 changes: 32 additions & 0 deletions src/Duration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import moment, {Duration as MomentDuration} from 'moment';
import {Instant} from "./Instant";
import {DurationSpec} from "./DurationSpec";
import {requireInt} from "./util/requireInt";

export class Duration
{
Expand Down Expand Up @@ -27,6 +29,36 @@ export class Duration
return this.momentDuration.toISOString();
}

public static of(
{
years = 0,
days = 0,
hours = 0,
minutes = 0,
seconds = 0,
millis = 0,
}: DurationSpec
)
{
requireInt(
seconds * 1000,
`Not accepting values with precision <1/1000ths of seconds; got ${seconds}`
);
requireInt(
millis,
`Only accepting whole milliseconds; got ${millis}`
);
return Duration.parse(
`P`
+ `${years}Y`
+ `${days}D`
+ 'T'
+ `${hours}H`
+ `${minutes}M`
+ `${seconds + (millis / 1000)}S`
);
}

public static ofYears(years: number)
{
return Duration.parse(`P${years}Y`);
Expand Down
9 changes: 9 additions & 0 deletions src/DurationSpec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface DurationSpec
{
years?: number
days?: number
hours?: number
minutes?: number
seconds?: number
millis?: number
}
92 changes: 68 additions & 24 deletions src/Instant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,51 @@ import {ZoneId, ZoneIdString} from "./ZoneId";
import {requireValidDate} from "./util/requireValidDate";
import {requireInt} from "./util/requireInt";
import {toZoneId} from "./util/toZoneId";
import {DurationSpec} from "./DurationSpec";

export class Instant
{
private readonly epochMilli: number;
public static readonly EPOCH = new Instant(0, 0);
private readonly secondsSinceEpoch: number;
private readonly microsInSecond: number;

private constructor(epochMilli: number)
private constructor(secondsSinceEpoch: number, microsInSecond: number)
{
this.epochMilli = requireInt(epochMilli);
this.secondsSinceEpoch = requireInt(
secondsSinceEpoch,
`Only a whole number allowed for the number of seconds since the epoch; got ${secondsSinceEpoch}`
);
this.microsInSecond = requireInt(
microsInSecond,
`Only a whole number allowed for the number of microseconds in the current second; got ${microsInSecond}`
);
}

public static now()
{
return new Instant(new Date().getTime());
return Instant.ofEpochMilli(Date.now());
}

public static parse(stringValue: string)
{
const date = new Date(stringValue);
return new Instant(
requireValidDate(date).getTime()
const date = requireValidDate(
new Date(stringValue)
);
return Instant.ofEpochMilli(date.getTime());
}

public static from(date: Date)
{
return new Instant(
return Instant.ofEpochMilli(
requireValidDate(date).getTime()
);
}

public static ofEpochMilli(epochMilli: number)
{
return new Instant(
requireInt(
epochMilli,
`Expected valid integer for epochMilli, but got ${epochMilli}`
)
);
const secondsSinceEpoch = Math.floor(epochMilli / 1000);
const microsInSecond = (epochMilli % 1000) * 1000;
return new Instant(secondsSinceEpoch, microsInSecond);
}

public static ofEpochSecond(epochSecond: number)
Expand All @@ -51,13 +58,19 @@ export class Instant
`Expected valid integer for epoch second, but got ${epochSecond}`
);
return new Instant(
validEpochSecond * 1000
validEpochSecond,
0,
);
}

/**
* Alias for {@link .plus}
*/
public add(duration: DurationSpec): Instant
public add(duration: Duration): Instant
public add(duration: Duration | DurationSpec): Instant
{
return new Instant(this.epochMilli + duration.asMillis);
return this.plus(duration);
}

public atZone(zoneIdString: ZoneIdString): ZonedDateTime
Expand All @@ -67,9 +80,14 @@ export class Instant
return new ZonedDateTime(this.toJS(), toZoneId(arg));
}

public plus(duration: DurationSpec): Instant
public plus(duration: Duration): Instant
public plus(duration: Duration | DurationSpec): Instant
{
return Instant.ofEpochMilli(this.epochMilli + duration.asMillis);
const asDuration = duration instanceof Duration
? duration
: Duration.of(duration);
return Instant.ofEpochMilli(this.toEpochMilli() + asDuration.asMillis);
}

public plusSeconds(secondsToAdd: number)
Expand All @@ -92,9 +110,24 @@ export class Instant
))
}

/**
* Alias for {@link .minus}
*/
public subtract(duration: DurationSpec): Instant
public subtract(duration: Duration): Instant
public subtract(duration: Duration | DurationSpec): Instant
{
return this.minus(duration);
}

public minus(duration: DurationSpec): Instant
public minus(duration: Duration): Instant
public minus(duration: Duration | DurationSpec): Instant
{
return Instant.ofEpochMilli(this.epochMilli - duration.asMillis);
const asDuration = duration instanceof Duration
? duration
: Duration.of(duration);
return Instant.ofEpochMilli(this.toEpochMilli() - asDuration.asMillis);
}

public minusSeconds(secondsToAdd: number)
Expand Down Expand Up @@ -124,7 +157,7 @@ export class Instant
*/
public compareTo(otherInstant: Instant): number
{
return this.epochMilli - otherInstant.epochMilli;
return this.toEpochMicro() - otherInstant.toEpochMicro();
}

public isAfter(otherInstant: Instant): boolean
Expand All @@ -144,15 +177,26 @@ export class Instant
{
if (this === otherInstant)
return true;
return this.epochMilli === otherInstant.epochMilli;
return this.secondsSinceEpoch === otherInstant.secondsSinceEpoch
&& this.microsInSecond === otherInstant.microsInSecond;
}

/**
* Gets the number of milliseconds from the Java epoch of 1970-01-01T00:00:00Z.
* Gets the number of milliseconds from the Java epoch of 1970-01-01T00:00:00Z. Microseconds, if present, are
* rounded down, so this method returns an integer.
*/
public toEpochMilli(): number
{
return this.epochMilli;
return this.toEpochMicro() / 1000;
}

/**
* In most cases simply a thousandfold of {@link .toEpochMilli()}, this may return a different value than 1000times
* {@link .toEpochMilli()} if this {@link Instant} was instantiated with a timestamp with sub-millisecond precision.
*/
public toEpochMicro(): number
{
return this.secondsSinceEpoch * 1000_000 + this.microsInSecond;
}

/**
Expand All @@ -169,13 +213,13 @@ export class Instant
*/
public getEpochSecond(): number
{
return Math.floor(this.epochMilli / 1000);
return this.secondsSinceEpoch;
}

public toJS(): Date
{
return requireValidDate(
new Date(this.epochMilli)
new Date(this.toEpochMilli())
);
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/util/requireInt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {isInt} from "./isInt";
export function requireInt(numberValue: number, message?: string): number
{
if (!isInt(numberValue))
{
throw new Error(message ?? `Expected value '${numberValue}' to be an integer`);
}
return numberValue;
}
38 changes: 38 additions & 0 deletions tests/Duration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {Duration} from "../dist";
import {assert} from "chai";
import {describe} from "mocha";

describe('Duration', () => {
describe('.of factory method', () => {
it('accepts whole years', () => {
assert.equal(Duration.of({years: 2}).toString(), 'P2Y');
})
it('accepts whole fractional years', () => {
assert.equal(Duration.of({years: 2.5}).toString(), 'P2Y6M');
})
it('accepts whole negative whole years', () => {
assert.equal(Duration.of({years: -2}).toString(), '-P2Y');
})
it('accepts whole negative fractional years', () => {
assert.equal(Duration.of({years: -2.5}).toString(), '-P2Y6M');
})
it('accepts whole days', () => {
assert.equal(Duration.of({days: 2}).toString(), 'P2D');
})
it('accepts whole hours', () => {
assert.equal(Duration.of({hours: 2}).toString(), 'PT2H');
})
it('accepts whole minutes', () => {
assert.equal(Duration.of({minutes: 2}).toString(), 'PT2M');
})
it('accepts whole seconds', () => {
assert.equal(Duration.of({seconds: 2}).toString(), 'PT2S');
})
it('accepts 1/1000ths of seconds', () => {
assert.equal(Duration.of({seconds: 0.002}).toString(), 'PT0.002S');
})
it('throws error when attempting to enter <1/1000ths of seconds', () => {
assert.throws(() => Duration.of({seconds: 0.0002}));
})
});
});
Loading

0 comments on commit b330f93

Please sign in to comment.