Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/reference/timestamp.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ constructor(value?: number | Date | Timestamp | string)

## Validation

`undefined` creates the current time; `null` returns a NullObject.

String values are parsed with `new Date(value)`. Inputs that produce an invalid ECMAScript `Date`, including non-finite and out-of-range millisecond values, are rejected with `InvalidNumberError`.

## Methods
Expand All @@ -36,7 +38,7 @@ String values are parsed with `new Date(value)`. Inputs that produce an invalid
| `static new(value)` | Creates a new `Timestamp`. |
| `static now()` | Creates a timestamp for the current time. |
| `static fromSeconds(seconds)` | Creates from seconds. |
| `toExactHour()` | Returns a timestamp rounded down to the exact UTC hour. |
| `toExactHour()` | Returns a timestamp rounded down to the exact UTC hour, independent of the local timezone. |
| `toMilliseconds()` | Returns milliseconds. |
| `toSeconds()` | Returns rounded seconds. |
| `toDate()` | Returns a `Date`. |
Expand Down
17 changes: 8 additions & 9 deletions src/value-objects/time/Timestamp.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { InvalidIntegerError } from '../../errors/InvalidIntegerError';
import { InvalidNumberError } from '../../errors/InvalidNumberError';
import { assert } from '../../patterns/Assert';
import { NullObject } from '../NullObject';
import { NumberValueObject } from '../NumberValueObject';
import { ValueObject } from '../ValueObject';
import { CalendarDay } from './CalendarDay';
Expand Down Expand Up @@ -97,7 +98,11 @@ export class Timestamp extends ValueObject<number> {
}

public constructor(value?: TimestampValue) {
super(Timestamp.valueMilliseconds(value));
super(value === null ? null : Timestamp.valueMilliseconds(value));

if (NullObject.isNullObject(this)) {
return this;
}

assert(
Number.isFinite(new Date(this.value).valueOf()),
Expand All @@ -106,15 +111,9 @@ export class Timestamp extends ValueObject<number> {
}

public toExactHour(): Timestamp {
const date = new Date(this.valueOf());

date.setSeconds(0);
date.setMinutes(0);
date.setMilliseconds(0);
const hours = Math.floor(this.value / Timestamp.FACTORS.HOURS);

const value = Timestamp.new(date.getTime()).value;

return this.clone(value);
return this.clone(hours * Timestamp.FACTORS.HOURS);
}

public toMilliseconds(): number {
Expand Down
27 changes: 27 additions & 0 deletions tests/value-objects/time/Timestamp.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
NullObject,
NullObjectError,
Timestamp,
Year,
Month,
Expand Down Expand Up @@ -514,3 +516,28 @@ describe('Timestamp', () => {
});
});
});

describe('Timestamp toExactHour and null', () => {
it('should floor to the UTC hour regardless of the local timezone', () => {
const timestamp = new Timestamp('2024-05-06T09:35:53.205Z');

expect(timestamp.toExactHour().toDate().toISOString()).toBe(
'2024-05-06T09:00:00.000Z',
);
});

it('should floor negative timestamps towards the previous hour', () => {
expect(new Timestamp(-1).toExactHour().valueOf()).toBe(-3600000);
});

it('should return a NullObject when null is received', () => {
const timestamp = new Timestamp(null as unknown as number);

expect(NullObject.isNullObject(timestamp)).toBeTrue();
expect(() => timestamp.toExactHour()).toThrow(NullObjectError);
});

it('should still default to now when undefined is received', () => {
expect(NullObject.isNullObject(new Timestamp(undefined))).toBeFalse();
});
});
Loading