Skip to content

Commit 2b1bdf9

Browse files
authored
fix: Fix and refactor Date (AssemblyScript#1804)
1 parent bde7278 commit 2b1bdf9

File tree

6 files changed

+6518
-2868
lines changed

6 files changed

+6518
-2868
lines changed

std/assembly/date.ts

Lines changed: 158 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
1-
import { E_VALUEOUTOFRANGE } from "util/error";
1+
import { E_INVALIDDATE } from "util/error";
22
import { now as Date_now } from "./bindings/Date";
33

4+
// @ts-ignore: decorator
5+
@inline const
6+
MILLIS_PER_DAY = 1000 * 60 * 60 * 24,
7+
MILLIS_PER_HOUR = 1000 * 60 * 60,
8+
MILLIS_PER_MINUTE = 1000 * 60,
9+
MILLIS_PER_SECOND = 1000;
10+
11+
// ymdFromEpochDays returns values via globals to avoid allocations
12+
// @ts-ignore: decorator
13+
@lazy let _month: i32, _day: i32;
14+
415
export class Date {
16+
private year: i32 = 0;
17+
private month: i32 = 0;
18+
private day: i32 = 0;
19+
520
@inline static UTC(
621
year: i32,
722
month: i32 = 0,
@@ -11,155 +26,177 @@ export class Date {
1126
second: i32 = 0,
1227
millisecond: i32 = 0
1328
): i64 {
14-
return epochMillis(year, month + 1, day, hour, minute, second, millisecond);
29+
if (year >= 0 && year <= 99) year += 1900;
30+
var ms = epochMillis(year, month + 1, day, hour, minute, second, millisecond);
31+
if (invalidDate(ms)) throw new RangeError(E_INVALIDDATE);
32+
return ms;
1533
}
1634

1735
@inline static now(): i64 {
1836
return <i64>Date_now();
1937
}
2038

21-
static fromString(dateTimeString: string): Date {
22-
let hour: i32 = 0,
23-
minute: i32 = 0,
24-
second: i32 = 0,
25-
millisecond: i32 = 0;
26-
let dateString: string;
39+
// It can parse only ISO 8601 inputs like YYYY-MM-DDTHH:MM:SS.000Z
40+
@inline static parse(dateString: string): Date {
41+
return this.fromString(dateString);
42+
}
2743

28-
if (dateTimeString.includes("T")) {
44+
static fromString(dateTimeString: string): Date {
45+
if (!dateTimeString.length) throw new RangeError(E_INVALIDDATE);
46+
var
47+
hour: i32 = 0,
48+
min: i32 = 0,
49+
sec: i32 = 0,
50+
ms: i32 = 0;
51+
52+
var dateString = dateTimeString;
53+
var posT = dateTimeString.indexOf("T");
54+
if (~posT) {
2955
// includes a time component
30-
const parts = dateTimeString.split("T");
31-
const timeString = parts[1];
56+
let timeString: string;
57+
dateString = dateTimeString.substring(0, posT);
58+
timeString = dateTimeString.substring(posT + 1);
3259
// parse the HH-MM-SS component
33-
const timeParts = timeString.split(":");
60+
let timeParts = timeString.split(":");
61+
let len = timeParts.length;
62+
if (len <= 1) throw new RangeError(E_INVALIDDATE);
63+
3464
hour = I32.parseInt(timeParts[0]);
35-
minute = I32.parseInt(timeParts[1]);
36-
if (timeParts[2].includes(".")) {
37-
// includes milliseconds
38-
const secondParts = timeParts[2].split(".");
39-
second = I32.parseInt(secondParts[0]);
40-
millisecond = I32.parseInt(secondParts[1]);
41-
} else {
42-
second = I32.parseInt(timeParts[2]);
65+
min = I32.parseInt(timeParts[1]);
66+
if (len >= 3) {
67+
let secAndMs = timeParts[2];
68+
let posDot = secAndMs.indexOf(".");
69+
if (~posDot) {
70+
// includes milliseconds
71+
sec = I32.parseInt(secAndMs.substring(0, posDot));
72+
ms = I32.parseInt(secAndMs.substring(posDot + 1));
73+
} else {
74+
sec = I32.parseInt(secAndMs);
75+
}
4376
}
44-
dateString = parts[0];
45-
} else {
46-
dateString = dateTimeString;
4777
}
4878
// parse the YYYY-MM-DD component
49-
const parts = dateString.split("-");
50-
const year = I32.parseInt(
51-
parts[0].length == 2 ? "19" + parts[0] : parts[0]
52-
);
53-
const month = I32.parseInt(parts[1]);
54-
const day = I32.parseInt(parts[2]);
55-
56-
return new Date(
57-
epochMillis(year, month, day, hour, minute, second, millisecond)
58-
);
79+
var parts = dateString.split("-");
80+
var year = I32.parseInt(parts[0]);
81+
var month = 1, day = 1;
82+
var len = parts.length;
83+
if (len >= 2) {
84+
month = I32.parseInt(parts[1]);
85+
if (len >= 3) {
86+
day = I32.parseInt(parts[2]);
87+
}
88+
}
89+
return new Date(epochMillis(year, month, day, hour, min, sec, ms));
5990
}
6091

61-
private epochMillis: i64;
92+
constructor(private epochMillis: i64) {
93+
// this differs from JavaScript which prefer return NaN or "Invalid Date" string
94+
// instead throwing exception.
95+
if (invalidDate(epochMillis)) throw new RangeError(E_INVALIDDATE);
6296

63-
constructor(epochMillis: i64) {
64-
this.epochMillis = epochMillis;
97+
this.year = ymdFromEpochDays(i32(floorDiv(epochMillis, MILLIS_PER_DAY)));
98+
this.month = _month;
99+
this.day = _day;
65100
}
66101

67102
getTime(): i64 {
68103
return this.epochMillis;
69104
}
70105

71-
setTime(value: i64): i64 {
72-
this.epochMillis = value;
73-
return value;
106+
setTime(time: i64): i64 {
107+
if (invalidDate(time)) throw new RangeError(E_INVALIDDATE);
108+
109+
this.epochMillis = time;
110+
this.year = ymdFromEpochDays(i32(floorDiv(time, MILLIS_PER_DAY)));
111+
this.month = _month;
112+
this.day = _day;
113+
114+
return time;
74115
}
75116

76117
getUTCFullYear(): i32 {
77-
ymdFromEpochDays(i32(this.epochMillis / MILLIS_PER_DAY));
78-
return year;
118+
return this.year;
79119
}
80120

81121
getUTCMonth(): i32 {
82-
ymdFromEpochDays(i32(this.epochMillis / MILLIS_PER_DAY));
83-
return month - 1;
122+
return this.month - 1;
84123
}
85124

86125
getUTCDate(): i32 {
87-
ymdFromEpochDays(i32(this.epochMillis / MILLIS_PER_DAY));
88-
return day;
126+
return this.day;
127+
}
128+
129+
getUTCDay(): i32 {
130+
return dayOfWeek(this.year, this.month, this.day);
89131
}
90132

91133
getUTCHours(): i32 {
92-
return i32(this.epochMillis % MILLIS_PER_DAY) / MILLIS_PER_HOUR;
134+
return i32(euclidRem(this.epochMillis, MILLIS_PER_DAY)) / MILLIS_PER_HOUR;
93135
}
94136

95137
getUTCMinutes(): i32 {
96-
return i32(this.epochMillis % MILLIS_PER_HOUR) / MILLIS_PER_MINUTE;
138+
return i32(euclidRem(this.epochMillis, MILLIS_PER_HOUR)) / MILLIS_PER_MINUTE;
97139
}
98140

99141
getUTCSeconds(): i32 {
100-
return i32(this.epochMillis % MILLIS_PER_MINUTE) / MILLIS_PER_SECOND;
142+
return i32(euclidRem(this.epochMillis, MILLIS_PER_MINUTE)) / MILLIS_PER_SECOND;
101143
}
102144

103145
getUTCMilliseconds(): i32 {
104-
return i32(this.epochMillis % MILLIS_PER_SECOND);
146+
return i32(euclidRem(this.epochMillis, MILLIS_PER_SECOND));
105147
}
106148

107-
setUTCMilliseconds(value: i32): void {
108-
this.epochMillis += value - this.getUTCMilliseconds();
149+
setUTCMilliseconds(millis: i32): void {
150+
this.setTime(this.epochMillis + (millis - this.getUTCMilliseconds()));
109151
}
110152

111-
setUTCSeconds(value: i32): void {
112-
throwIfNotInRange(value, 0, 59);
113-
this.epochMillis += (value - this.getUTCSeconds()) * MILLIS_PER_SECOND;
153+
setUTCSeconds(seconds: i32): void {
154+
this.setTime(this.epochMillis + (seconds - this.getUTCSeconds()) * MILLIS_PER_SECOND);
114155
}
115156

116-
setUTCMinutes(value: i32): void {
117-
throwIfNotInRange(value, 0, 59);
118-
this.epochMillis += (value - this.getUTCMinutes()) * MILLIS_PER_MINUTE;
157+
setUTCMinutes(minutes: i32): void {
158+
this.setTime(this.epochMillis + (minutes - this.getUTCMinutes()) * MILLIS_PER_MINUTE);
119159
}
120160

121-
setUTCHours(value: i32): void {
122-
throwIfNotInRange(value, 0, 23);
123-
this.epochMillis += (value - this.getUTCHours()) * MILLIS_PER_HOUR;
161+
setUTCHours(hours: i32): void {
162+
this.setTime(this.epochMillis + (hours - this.getUTCHours()) * MILLIS_PER_HOUR);
124163
}
125164

126-
setUTCDate(value: i32): void {
127-
ymdFromEpochDays(i32(this.epochMillis / MILLIS_PER_DAY));
128-
throwIfNotInRange(value, 1, daysInMonth(year, month));
129-
const mills = this.epochMillis % MILLIS_PER_DAY;
130-
this.epochMillis =
131-
i64(daysSinceEpoch(year, month, value)) * MILLIS_PER_DAY + mills;
165+
setUTCDate(day: i32): void {
166+
if (this.day == day) return;
167+
var ms = euclidRem(this.epochMillis, MILLIS_PER_DAY);
168+
this.setTime(i64(daysSinceEpoch(this.year, this.month, day)) * MILLIS_PER_DAY + ms);
132169
}
133170

134-
setUTCMonth(value: i32): void {
135-
throwIfNotInRange(value, 1, 12);
136-
ymdFromEpochDays(i32(this.epochMillis / MILLIS_PER_DAY));
137-
const mills = this.epochMillis % MILLIS_PER_DAY;
138-
this.epochMillis =
139-
i64(daysSinceEpoch(year, value + 1, day)) * MILLIS_PER_DAY + mills;
171+
setUTCMonth(month: i32): void {
172+
if (this.month == month) return;
173+
var ms = euclidRem(this.epochMillis, MILLIS_PER_DAY);
174+
this.setTime(i64(daysSinceEpoch(this.year, month + 1, this.day)) * MILLIS_PER_DAY + ms);
140175
}
141176

142-
setUTCFullYear(value: i32): void {
143-
ymdFromEpochDays(i32(this.epochMillis / MILLIS_PER_DAY));
144-
const mills = this.epochMillis % MILLIS_PER_DAY;
145-
this.epochMillis =
146-
i64(daysSinceEpoch(value, month, day)) * MILLIS_PER_DAY + mills;
177+
setUTCFullYear(year: i32): void {
178+
if (this.year == year) return;
179+
var ms = euclidRem(this.epochMillis, MILLIS_PER_DAY);
180+
this.setTime(i64(daysSinceEpoch(year, this.month, this.day)) * MILLIS_PER_DAY + ms);
147181
}
148182

149183
toISOString(): string {
150-
ymdFromEpochDays(i32(this.epochMillis / MILLIS_PER_DAY));
151-
152-
let yearStr = year.toString();
153-
if (yearStr.length > 4) {
154-
yearStr = "+" + yearStr.padStart(6, "0");
184+
// TODO: add more low-level helper which combine toString and padStart without extra allocation
185+
var yearStr: string;
186+
var year = this.year;
187+
var isNeg = year < 0;
188+
if (isNeg || year >= 10000) {
189+
yearStr = (isNeg ? "-" : "+") + abs(year).toString().padStart(6, "0");
190+
} else {
191+
yearStr = year.toString().padStart(4, "0");
155192
}
156193

157194
return (
158195
yearStr +
159196
"-" +
160-
month.toString().padStart(2, "0") +
197+
this.month.toString().padStart(2, "0") +
161198
"-" +
162-
day.toString().padStart(2, "0") +
199+
this.day.toString().padStart(2, "0") +
163200
"T" +
164201
this.getUTCHours().toString().padStart(2, "0") +
165202
":" +
@@ -191,48 +228,54 @@ function epochMillis(
191228
);
192229
}
193230

194-
function throwIfNotInRange(value: i32, lower: i32, upper: i32): void {
195-
if (value < lower || value > upper) throw new RangeError(E_VALUEOUTOFRANGE);
231+
// @ts-ignore: decorator
232+
@inline function floorDiv<T extends number>(a: T, b: T): T {
233+
return (a >= 0 ? a : a - b + 1) / b as T;
196234
}
197235

198-
const MILLIS_PER_DAY = 1_000 * 60 * 60 * 24;
199-
const MILLIS_PER_HOUR = 1_000 * 60 * 60;
200-
const MILLIS_PER_MINUTE = 1_000 * 60;
201-
const MILLIS_PER_SECOND = 1_000;
202-
203-
// http://howardhinnant.github.io/date_algorithms.html#is_leap
204-
function isLeap(y: i32): bool {
205-
return y % 4 == 0 && (y % 100 != 0 || y % 400 == 0);
236+
// @ts-ignore: decorator
237+
@inline function euclidRem<T extends number>(a: T, b: T): T {
238+
var m = a % b;
239+
return m + (m < 0 ? b : 0) as T;
206240
}
207241

208-
function daysInMonth(year: i32, month: i32): i32 {
209-
return month == 2
210-
? 28 + i32(isLeap(year))
211-
: 30 + ((month + i32(month >= 8)) & 1);
242+
function invalidDate(millis: i64): bool {
243+
// @ts-ignore
244+
return (millis < -8640000000000000) | (millis > 8640000000000000);
212245
}
213246

214-
// ymdFromEpochDays returns values via globals to avoid allocations
215-
let year: i32, month: i32, day: i32;
216247
// see: http://howardhinnant.github.io/date_algorithms.html#civil_from_days
217-
function ymdFromEpochDays(z: i32): void {
248+
function ymdFromEpochDays(z: i32): i32 {
218249
z += 719468;
219-
const era = (z >= 0 ? z : z - 146096) / 146097;
220-
const doe = z - era * 146097; // [0, 146096]
221-
const yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
222-
year = yoe + era * 400;
223-
const doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
224-
const mp = (5 * doy + 2) / 153; // [0, 11]
225-
day = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
226-
month = mp + (mp < 10 ? 3 : -9); // [1, 12]
227-
year += (month <= 2 ? 1 : 0);
250+
var era = <u32>floorDiv(z, 146097);
251+
var doe = <u32>z - era * 146097; // [0, 146096]
252+
var yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
253+
var year = yoe + era * 400;
254+
var doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
255+
var mo = (5 * doy + 2) / 153; // [0, 11]
256+
_day = doy - (153 * mo + 2) / 5 + 1; // [1, 31]
257+
mo += mo < 10 ? 3 : -9; // [1, 12]
258+
_month = mo;
259+
year += u32(mo <= 2);
260+
return year;
228261
}
229262

230263
// http://howardhinnant.github.io/date_algorithms.html#days_from_civil
231264
function daysSinceEpoch(y: i32, m: i32, d: i32): i32 {
232-
y -= m <= 2 ? 1 : 0;
233-
const era = (y >= 0 ? y : y - 399) / 400;
234-
const yoe = y - era * 400; // [0, 399]
235-
const doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; // [0, 365]
236-
const doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096]
265+
y -= i32(m <= 2);
266+
var era = <u32>floorDiv(y, 400);
267+
var yoe = <u32>y - era * 400; // [0, 399]
268+
var doy = <u32>(153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; // [0, 365]
269+
var doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096]
237270
return era * 146097 + doe - 719468;
238271
}
272+
273+
// TomohikoSakamoto algorithm from https://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week
274+
function dayOfWeek(year: i32, month: i32, day: i32): i32 {
275+
const tab = memory.data<u8>([0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4]);
276+
277+
year -= i32(month < 3);
278+
year += year / 4 - year / 100 + year / 400;
279+
month = <i32>load<u8>(tab + month - 1);
280+
return euclidRem(year + month + day, 7);
281+
}

std/assembly/index.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1723,7 +1723,9 @@ declare class Date {
17231723
): i64;
17241724
/** Returns the current UTC timestamp in milliseconds. */
17251725
static now(): i64;
1726-
static fromString(dateStr: string): Date;
1726+
/** Parses a string representation of a date, and returns the number of milliseconds since January 1, 1970, 00:00:00 UTC. */
1727+
static parse(dateString: string): Date;
1728+
static fromString(dateString: string): Date;
17271729
/** Constructs a new date object from an UTC timestamp in milliseconds. */
17281730
constructor(value: i64);
17291731
/** Returns the UTC timestamp of this date in milliseconds. */
@@ -1734,6 +1736,7 @@ declare class Date {
17341736
getUTCFullYear(): i32;
17351737
getUTCMonth(): i32;
17361738
getUTCDate(): i32;
1739+
getUTCDay(): i32;
17371740
getUTCHours(): i32;
17381741
getUTCMinutes(): i32;
17391742
getUTCSeconds(): i32;

std/assembly/util/error.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,7 @@ export const E_NOT_PINNED: string = "Object is not pinned";
4848
// @ts-ignore: decorator
4949
@lazy @inline
5050
export const E_URI_MALFORMED: string = "URI malformed";
51+
52+
// @ts-ignore: decorator
53+
@lazy @inline
54+
export const E_INVALIDDATE: string = "Invalid Date";

0 commit comments

Comments
 (0)