Skip to content

Commit 900d1b7

Browse files
authored
Merge pull request #801 from Cratis:feature/time_span
Introducing TimeSpan for TypeScript with serialization support
2 parents 791e26a + 0d88799 commit 900d1b7

13 files changed

+305
-1
lines changed

Source/JavaScript/JsonSerializer.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { DerivedType } from './DerivedType';
66
import { Field } from './Field';
77
import { Fields } from './Fields';
88
import { Guid } from './Guid';
9+
import { TimeSpan } from './TimeSpan';
910

1011
/* eslint-disable @typescript-eslint/no-explicit-any */
1112

@@ -16,7 +17,8 @@ const typeConverters: Map<Constructor, typeSerializer> = new Map<Constructor, ty
1617
[String, (value: string) => value],
1718
[Boolean, (value: boolean) => value],
1819
[Date, (value: Date) => value.toISOString()],
19-
[Guid, (value: Guid) => value?.toString() ?? '']
20+
[Guid, (value: Guid) => value?.toString() ?? ''],
21+
[TimeSpan, (value: TimeSpan) => value?.toString() ?? '']
2022
]);
2123

2224
const typeSerializers: Map<Constructor, typeSerializer> = new Map<Constructor, typeSerializer>([
@@ -25,6 +27,7 @@ const typeSerializers: Map<Constructor, typeSerializer> = new Map<Constructor, t
2527
[Boolean, (value: any) => value],
2628
[Date, (value: any) => new Date(value)],
2729
[Guid, (value: any) => Guid.parse(value.toString())],
30+
[TimeSpan, (value: any) => TimeSpan.parse(value.toString())],
2831
]);
2932

3033
const serializeValueForType = (type: Constructor, value: any) => {

Source/JavaScript/TimeSpan.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Copyright (c) Cratis. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
import { field } from './fieldDecorator';
5+
6+
const timeSpanRegex = /^(-)?(?:(\d+)\.)?(\d{1,2}):(\d{2}):(\d{2})(?:\.(\d{1,7}))?$/;
7+
8+
/**
9+
* Represents a time interval.
10+
*/
11+
export class TimeSpan {
12+
13+
@field(Number)
14+
ticks!: number;
15+
16+
@field(Number)
17+
days!: number;
18+
19+
@field(Number)
20+
hours!: number;
21+
22+
@field(Number)
23+
milliseconds!: number;
24+
25+
@field(Number)
26+
microseconds!: number;
27+
28+
@field(Number)
29+
nanoseconds!: number;
30+
31+
@field(Number)
32+
minutes!: number;
33+
34+
@field(Number)
35+
seconds!: number;
36+
37+
@field(Number)
38+
totalDays!: number;
39+
40+
@field(Number)
41+
totalHours!: number;
42+
43+
@field(Number)
44+
totalMilliseconds!: number;
45+
46+
@field(Number)
47+
totalMicroseconds!: number;
48+
49+
@field(Number)
50+
totalNanoseconds!: number;
51+
52+
@field(Number)
53+
totalMinutes!: number;
54+
55+
@field(Number)
56+
totalSeconds!: number;
57+
58+
/**
59+
* Parses a C# compatible TimeSpan string and returns a TimeSpan instance.
60+
* The string format should match: [-][d.]hh:mm:ss[.fffffff]
61+
* @param {string} value String representation of TimeSpan.
62+
* @returns {TimeSpan} A TimeSpan instance with all properties populated.
63+
* @throws {Error} If the string format is invalid.
64+
*/
65+
static parse(value: string): TimeSpan {
66+
const match = timeSpanRegex.exec(value);
67+
if (match === null) {
68+
throw new Error(`Invalid TimeSpan format: ${value}`);
69+
}
70+
71+
const isNegative = match[1] === '-';
72+
const days = match[2] ? parseInt(match[2], 10) : 0;
73+
const hours = parseInt(match[3], 10);
74+
const minutes = parseInt(match[4], 10);
75+
const seconds = parseInt(match[5], 10);
76+
const fractionalSeconds = match[6] ? match[6].padEnd(7, '0') : '0000000';
77+
78+
const ticksPerDay = 864000000000;
79+
const ticksPerHour = 36000000000;
80+
const ticksPerMinute = 600000000;
81+
const ticksPerSecond = 10000000;
82+
83+
const ticks =
84+
days * ticksPerDay +
85+
hours * ticksPerHour +
86+
minutes * ticksPerMinute +
87+
seconds * ticksPerSecond +
88+
parseInt(fractionalSeconds, 10);
89+
90+
const finalTicks = isNegative ? -ticks : ticks;
91+
92+
const timeSpan = new TimeSpan();
93+
timeSpan.ticks = finalTicks;
94+
timeSpan.days = Math.floor(Math.abs(finalTicks) / ticksPerDay) * (isNegative ? -1 : 1);
95+
timeSpan.hours = Math.floor((Math.abs(finalTicks) / ticksPerHour) % 24);
96+
timeSpan.minutes = Math.floor((Math.abs(finalTicks) / ticksPerMinute) % 60);
97+
timeSpan.seconds = Math.floor((Math.abs(finalTicks) / ticksPerSecond) % 60);
98+
timeSpan.milliseconds = Math.floor((Math.abs(finalTicks) / 10000) % 1000);
99+
timeSpan.microseconds = Math.floor((Math.abs(finalTicks) / 10) % 1000);
100+
timeSpan.nanoseconds = Math.floor((Math.abs(finalTicks) % 10) * 100);
101+
timeSpan.totalDays = finalTicks / ticksPerDay;
102+
timeSpan.totalHours = finalTicks / ticksPerHour;
103+
timeSpan.totalMinutes = finalTicks / ticksPerMinute;
104+
timeSpan.totalSeconds = finalTicks / ticksPerSecond;
105+
timeSpan.totalMilliseconds = finalTicks / 10000;
106+
timeSpan.totalMicroseconds = finalTicks / 10;
107+
timeSpan.totalNanoseconds = finalTicks * 100;
108+
109+
return timeSpan;
110+
}
111+
112+
/**
113+
* Converts the TimeSpan to a C# compatible string format.
114+
* Returns the TimeSpan in format: [-][d.]hh:mm:ss[.fffffff]
115+
* @returns {string} C# compatible TimeSpan string representation.
116+
*/
117+
toString(): string {
118+
const isNegative = this.ticks < 0;
119+
const absTicks = Math.abs(this.ticks);
120+
121+
const ticksPerDay = 864000000000;
122+
const ticksPerHour = 36000000000;
123+
const ticksPerMinute = 600000000;
124+
const ticksPerSecond = 10000000;
125+
126+
const days = Math.floor(absTicks / ticksPerDay);
127+
const hours = Math.floor((absTicks / ticksPerHour) % 24);
128+
const minutes = Math.floor((absTicks / ticksPerMinute) % 60);
129+
const seconds = Math.floor((absTicks / ticksPerSecond) % 60);
130+
const fractionalTicks = absTicks % ticksPerSecond;
131+
132+
const parts: string[] = [];
133+
134+
if (isNegative) {
135+
parts.push('-');
136+
}
137+
138+
if (days > 0) {
139+
parts.push(`${days}.`);
140+
}
141+
142+
parts.push(`${hours.toString().padStart(2, '0')}:`);
143+
parts.push(`${minutes.toString().padStart(2, '0')}:`);
144+
parts.push(`${seconds.toString().padStart(2, '0')}`);
145+
146+
if (fractionalTicks > 0) {
147+
const fractionalStr = fractionalTicks.toString().padStart(7, '0').replace(/0+$/, '');
148+
parts.push(`.${fractionalStr}`);
149+
}
150+
151+
return parts.join('');
152+
}
153+
154+
/**
155+
* Converts the TimeSpan to a JSON string that works in JSON serialization.
156+
* Returns the TimeSpan in C# compatible format: [-][d.]hh:mm:ss[.fffffff]
157+
* @returns {string} C# compatible TimeSpan string representation.
158+
*/
159+
toJSON(): string {
160+
return this.toString();
161+
}
162+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) Cratis. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
import { TimeSpan } from '../TimeSpan';
5+
6+
describe('when calculating total values', () => {
7+
const timeSpanString = '1.02:30:45';
8+
const parsed = TimeSpan.parse(timeSpanString);
9+
10+
it('should have correct total hours', () => parsed.totalHours.should.equal(26.5125));
11+
it('should have correct total minutes', () => parsed.totalMinutes.should.equal(1590.75));
12+
it('should have correct total seconds', () => parsed.totalSeconds.should.equal(95445));
13+
it('should have correct total milliseconds', () => parsed.totalMilliseconds.should.equal(95445000));
14+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Cratis. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
import { TimeSpan } from '../TimeSpan';
5+
6+
describe('when converting simple time to string', () => {
7+
const originalString = '00:30:15';
8+
const parsed = TimeSpan.parse(originalString);
9+
const convertedBack = parsed.toString();
10+
11+
it('should match original string', () => convertedBack.should.equal(originalString));
12+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Cratis. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
import { TimeSpan } from '../TimeSpan';
5+
6+
describe('when converting to json', () => {
7+
const originalString = '3.08:45:30.5';
8+
const parsed = TimeSpan.parse(originalString);
9+
const json = parsed.toJSON();
10+
11+
it('should return string format', () => json.should.equal(originalString));
12+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Cratis. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
import { TimeSpan } from '../TimeSpan';
5+
6+
describe('when converting to string', () => {
7+
const originalString = '5.12:30:45.1234567';
8+
const parsed = TimeSpan.parse(originalString);
9+
const convertedBack = parsed.toString();
10+
11+
it('should match original string', () => convertedBack.should.equal(originalString));
12+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) Cratis. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
import { TimeSpan } from '../TimeSpan';
5+
6+
describe('when parsing negative time', () => {
7+
const timeSpanString = '-2.03:15:30';
8+
const parsed = TimeSpan.parse(timeSpanString);
9+
10+
it('should have negative days', () => parsed.days.should.equal(-2));
11+
it('should have correct hours', () => parsed.hours.should.equal(3));
12+
it('should have correct minutes', () => parsed.minutes.should.equal(15));
13+
it('should have correct seconds', () => parsed.seconds.should.equal(30));
14+
it('should have negative ticks', () => parsed.ticks.should.be.lessThan(0));
15+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) Cratis. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
import { TimeSpan } from '../TimeSpan';
5+
6+
describe('when parsing simple time', () => {
7+
const timeSpanString = '01:30:45';
8+
const parsed = TimeSpan.parse(timeSpanString);
9+
10+
it('should have correct hours', () => parsed.hours.should.equal(1));
11+
it('should have correct minutes', () => parsed.minutes.should.equal(30));
12+
it('should have correct seconds', () => parsed.seconds.should.equal(45));
13+
it('should have zero days', () => parsed.days.should.equal(0));
14+
it('should have zero milliseconds', () => parsed.milliseconds.should.equal(0));
15+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) Cratis. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
import { TimeSpan } from '../TimeSpan';
5+
6+
describe('when parsing time with days', () => {
7+
const timeSpanString = '5.12:30:45';
8+
const parsed = TimeSpan.parse(timeSpanString);
9+
10+
it('should have correct days', () => parsed.days.should.equal(5));
11+
it('should have correct hours', () => parsed.hours.should.equal(12));
12+
it('should have correct minutes', () => parsed.minutes.should.equal(30));
13+
it('should have correct seconds', () => parsed.seconds.should.equal(45));
14+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) Cratis. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
import { TimeSpan } from '../TimeSpan';
5+
6+
describe('when parsing time with fractional seconds', () => {
7+
const timeSpanString = '01:30:45.1234567';
8+
const parsed = TimeSpan.parse(timeSpanString);
9+
10+
it('should have correct hours', () => parsed.hours.should.equal(1));
11+
it('should have correct minutes', () => parsed.minutes.should.equal(30));
12+
it('should have correct seconds', () => parsed.seconds.should.equal(45));
13+
it('should have correct milliseconds', () => parsed.milliseconds.should.equal(123));
14+
it('should have correct microseconds', () => parsed.microseconds.should.equal(456));
15+
it('should have correct nanoseconds', () => parsed.nanoseconds.should.equal(700));
16+
});

0 commit comments

Comments
 (0)