Skip to content

fix: Fix and refactor Date #1804

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 52 commits into from
Apr 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
1db5851
init
MaxGraey Apr 14, 2021
657437d
simplify
MaxGraey Apr 14, 2021
1dfbe7f
refactor more
MaxGraey Apr 14, 2021
916f674
precalc and cache UTC date's values
MaxGraey Apr 14, 2021
e6fc2a6
refactor + more tests
MaxGraey Apr 14, 2021
795d014
more tests
MaxGraey Apr 14, 2021
cfa431b
more checks
MaxGraey Apr 14, 2021
264e666
fixes (WIP)
MaxGraey Apr 14, 2021
4715bc7
skip add guards
MaxGraey Apr 14, 2021
0cdc388
typo
MaxGraey Apr 14, 2021
b6a4ed6
more fixes and tests
MaxGraey Apr 14, 2021
00c0758
more
MaxGraey Apr 15, 2021
28ec3ec
more
MaxGraey Apr 15, 2021
6cf10c7
partially fix (wip)
MaxGraey Apr 15, 2021
e3b788d
more
MaxGraey Apr 15, 2021
c578d02
fixes done!
MaxGraey Apr 15, 2021
470d966
more fixes for toISOString
MaxGraey Apr 15, 2021
5a91cfe
fix guard check
MaxGraey Apr 15, 2021
2f2a0f4
minor opt toISOString
MaxGraey Apr 15, 2021
10ea880
more
MaxGraey Apr 15, 2021
028a67f
remove boundaries
MaxGraey Apr 15, 2021
ab9f9ac
opt floorMod for positive values
MaxGraey Apr 15, 2021
994d6b8
add guard for Date.UTC
MaxGraey Apr 15, 2021
cbf4579
remove inline from invalidDate
MaxGraey Apr 15, 2021
b53d9c6
use unsigned types for some ymdFromEpochDays and daysSinceEpoch ops
MaxGraey Apr 15, 2021
0c8aa0c
more parser fixes
MaxGraey Apr 15, 2021
e74c915
refactor
MaxGraey Apr 15, 2021
36ab8ba
more parser fixes
MaxGraey Apr 15, 2021
07b1b58
wip
MaxGraey Apr 15, 2021
9d8fc45
refactor
MaxGraey Apr 15, 2021
0d22062
fixes
MaxGraey Apr 15, 2021
56ecf2b
add Date.parse class method
MaxGraey Apr 15, 2021
b2c52c6
comment
MaxGraey Apr 15, 2021
ea5c3c2
Merge branch 'master' into date-refactor
MaxGraey Apr 15, 2021
171922f
Merge branch 'master' into date-refactor
MaxGraey Apr 17, 2021
3df0ef5
update fixtures
MaxGraey Apr 17, 2021
4f9142b
Merge branch 'master' into date-refactor
MaxGraey Apr 17, 2021
eb29f2d
more tests
MaxGraey Apr 18, 2021
acb3176
more special tests
MaxGraey Apr 18, 2021
fc58777
fixme later
MaxGraey Apr 18, 2021
bf82286
Merge branch 'master' into date-refactor
MaxGraey Apr 20, 2021
9cbb950
typo in comment
MaxGraey Apr 21, 2021
1bf570b
force ci
MaxGraey Apr 23, 2021
74a225c
works?
MaxGraey Apr 24, 2021
c57c91f
upd fixtures
MaxGraey Apr 24, 2021
99cf242
add getUTCDay
MaxGraey Apr 25, 2021
e23479a
remove export
MaxGraey Apr 25, 2021
ccc731b
fix
MaxGraey Apr 25, 2021
1db25ee
opt floorMod
MaxGraey Apr 25, 2021
18fdc7d
refactor dayOfWeek
MaxGraey Apr 25, 2021
2134f60
floorMod -> euclidRem
MaxGraey Apr 25, 2021
a7c86b6
Merge branch 'master' into date-refactor
MaxGraey Apr 25, 2021
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
273 changes: 158 additions & 115 deletions std/assembly/date.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import { E_VALUEOUTOFRANGE } from "util/error";
import { E_INVALIDDATE } from "util/error";
import { now as Date_now } from "./bindings/Date";

// @ts-ignore: decorator
@inline const
MILLIS_PER_DAY = 1000 * 60 * 60 * 24,
MILLIS_PER_HOUR = 1000 * 60 * 60,
MILLIS_PER_MINUTE = 1000 * 60,
MILLIS_PER_SECOND = 1000;

// ymdFromEpochDays returns values via globals to avoid allocations
// @ts-ignore: decorator
@lazy let _month: i32, _day: i32;

export class Date {
private year: i32 = 0;
private month: i32 = 0;
private day: i32 = 0;

@inline static UTC(
year: i32,
month: i32 = 0,
Expand All @@ -11,155 +26,177 @@ export class Date {
second: i32 = 0,
millisecond: i32 = 0
): i64 {
return epochMillis(year, month + 1, day, hour, minute, second, millisecond);
if (year >= 0 && year <= 99) year += 1900;
var ms = epochMillis(year, month + 1, day, hour, minute, second, millisecond);
if (invalidDate(ms)) throw new RangeError(E_INVALIDDATE);
return ms;
}

@inline static now(): i64 {
return <i64>Date_now();
}

static fromString(dateTimeString: string): Date {
let hour: i32 = 0,
minute: i32 = 0,
second: i32 = 0,
millisecond: i32 = 0;
let dateString: string;
// It can parse only ISO 8601 inputs like YYYY-MM-DDTHH:MM:SS.000Z
@inline static parse(dateString: string): Date {
return this.fromString(dateString);
}

if (dateTimeString.includes("T")) {
static fromString(dateTimeString: string): Date {
if (!dateTimeString.length) throw new RangeError(E_INVALIDDATE);
var
hour: i32 = 0,
min: i32 = 0,
sec: i32 = 0,
ms: i32 = 0;

var dateString = dateTimeString;
var posT = dateTimeString.indexOf("T");
if (~posT) {
// includes a time component
const parts = dateTimeString.split("T");
const timeString = parts[1];
let timeString: string;
dateString = dateTimeString.substring(0, posT);
timeString = dateTimeString.substring(posT + 1);
// parse the HH-MM-SS component
const timeParts = timeString.split(":");
let timeParts = timeString.split(":");
let len = timeParts.length;
if (len <= 1) throw new RangeError(E_INVALIDDATE);

hour = I32.parseInt(timeParts[0]);
minute = I32.parseInt(timeParts[1]);
if (timeParts[2].includes(".")) {
// includes milliseconds
const secondParts = timeParts[2].split(".");
second = I32.parseInt(secondParts[0]);
millisecond = I32.parseInt(secondParts[1]);
} else {
second = I32.parseInt(timeParts[2]);
min = I32.parseInt(timeParts[1]);
if (len >= 3) {
let secAndMs = timeParts[2];
let posDot = secAndMs.indexOf(".");
if (~posDot) {
// includes milliseconds
sec = I32.parseInt(secAndMs.substring(0, posDot));
ms = I32.parseInt(secAndMs.substring(posDot + 1));
} else {
sec = I32.parseInt(secAndMs);
}
}
dateString = parts[0];
} else {
dateString = dateTimeString;
}
// parse the YYYY-MM-DD component
const parts = dateString.split("-");
const year = I32.parseInt(
parts[0].length == 2 ? "19" + parts[0] : parts[0]
);
const month = I32.parseInt(parts[1]);
const day = I32.parseInt(parts[2]);

return new Date(
epochMillis(year, month, day, hour, minute, second, millisecond)
);
var parts = dateString.split("-");
var year = I32.parseInt(parts[0]);
var month = 1, day = 1;
var len = parts.length;
if (len >= 2) {
month = I32.parseInt(parts[1]);
if (len >= 3) {
day = I32.parseInt(parts[2]);
}
}
return new Date(epochMillis(year, month, day, hour, min, sec, ms));
}

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

constructor(epochMillis: i64) {
this.epochMillis = epochMillis;
this.year = ymdFromEpochDays(i32(floorDiv(epochMillis, MILLIS_PER_DAY)));
this.month = _month;
this.day = _day;
}

getTime(): i64 {
return this.epochMillis;
}

setTime(value: i64): i64 {
this.epochMillis = value;
return value;
setTime(time: i64): i64 {
if (invalidDate(time)) throw new RangeError(E_INVALIDDATE);

this.epochMillis = time;
this.year = ymdFromEpochDays(i32(floorDiv(time, MILLIS_PER_DAY)));
this.month = _month;
this.day = _day;

return time;
}

getUTCFullYear(): i32 {
ymdFromEpochDays(i32(this.epochMillis / MILLIS_PER_DAY));
return year;
return this.year;
}

getUTCMonth(): i32 {
ymdFromEpochDays(i32(this.epochMillis / MILLIS_PER_DAY));
return month - 1;
return this.month - 1;
}

getUTCDate(): i32 {
ymdFromEpochDays(i32(this.epochMillis / MILLIS_PER_DAY));
return day;
return this.day;
}

getUTCDay(): i32 {
return dayOfWeek(this.year, this.month, this.day);
}

getUTCHours(): i32 {
return i32(this.epochMillis % MILLIS_PER_DAY) / MILLIS_PER_HOUR;
return i32(euclidRem(this.epochMillis, MILLIS_PER_DAY)) / MILLIS_PER_HOUR;
}

getUTCMinutes(): i32 {
return i32(this.epochMillis % MILLIS_PER_HOUR) / MILLIS_PER_MINUTE;
return i32(euclidRem(this.epochMillis, MILLIS_PER_HOUR)) / MILLIS_PER_MINUTE;
}

getUTCSeconds(): i32 {
return i32(this.epochMillis % MILLIS_PER_MINUTE) / MILLIS_PER_SECOND;
return i32(euclidRem(this.epochMillis, MILLIS_PER_MINUTE)) / MILLIS_PER_SECOND;
}

getUTCMilliseconds(): i32 {
return i32(this.epochMillis % MILLIS_PER_SECOND);
return i32(euclidRem(this.epochMillis, MILLIS_PER_SECOND));
}

setUTCMilliseconds(value: i32): void {
this.epochMillis += value - this.getUTCMilliseconds();
setUTCMilliseconds(millis: i32): void {
this.setTime(this.epochMillis + (millis - this.getUTCMilliseconds()));
}

setUTCSeconds(value: i32): void {
throwIfNotInRange(value, 0, 59);
this.epochMillis += (value - this.getUTCSeconds()) * MILLIS_PER_SECOND;
setUTCSeconds(seconds: i32): void {
this.setTime(this.epochMillis + (seconds - this.getUTCSeconds()) * MILLIS_PER_SECOND);
}

setUTCMinutes(value: i32): void {
throwIfNotInRange(value, 0, 59);
this.epochMillis += (value - this.getUTCMinutes()) * MILLIS_PER_MINUTE;
setUTCMinutes(minutes: i32): void {
this.setTime(this.epochMillis + (minutes - this.getUTCMinutes()) * MILLIS_PER_MINUTE);
}

setUTCHours(value: i32): void {
throwIfNotInRange(value, 0, 23);
this.epochMillis += (value - this.getUTCHours()) * MILLIS_PER_HOUR;
setUTCHours(hours: i32): void {
this.setTime(this.epochMillis + (hours - this.getUTCHours()) * MILLIS_PER_HOUR);
}

setUTCDate(value: i32): void {
ymdFromEpochDays(i32(this.epochMillis / MILLIS_PER_DAY));
throwIfNotInRange(value, 1, daysInMonth(year, month));
const mills = this.epochMillis % MILLIS_PER_DAY;
this.epochMillis =
i64(daysSinceEpoch(year, month, value)) * MILLIS_PER_DAY + mills;
setUTCDate(day: i32): void {
if (this.day == day) return;
var ms = euclidRem(this.epochMillis, MILLIS_PER_DAY);
this.setTime(i64(daysSinceEpoch(this.year, this.month, day)) * MILLIS_PER_DAY + ms);
}

setUTCMonth(value: i32): void {
throwIfNotInRange(value, 1, 12);
ymdFromEpochDays(i32(this.epochMillis / MILLIS_PER_DAY));
const mills = this.epochMillis % MILLIS_PER_DAY;
this.epochMillis =
i64(daysSinceEpoch(year, value + 1, day)) * MILLIS_PER_DAY + mills;
setUTCMonth(month: i32): void {
if (this.month == month) return;
var ms = euclidRem(this.epochMillis, MILLIS_PER_DAY);
this.setTime(i64(daysSinceEpoch(this.year, month + 1, this.day)) * MILLIS_PER_DAY + ms);
}

setUTCFullYear(value: i32): void {
ymdFromEpochDays(i32(this.epochMillis / MILLIS_PER_DAY));
const mills = this.epochMillis % MILLIS_PER_DAY;
this.epochMillis =
i64(daysSinceEpoch(value, month, day)) * MILLIS_PER_DAY + mills;
setUTCFullYear(year: i32): void {
if (this.year == year) return;
var ms = euclidRem(this.epochMillis, MILLIS_PER_DAY);
this.setTime(i64(daysSinceEpoch(year, this.month, this.day)) * MILLIS_PER_DAY + ms);
}

toISOString(): string {
ymdFromEpochDays(i32(this.epochMillis / MILLIS_PER_DAY));

let yearStr = year.toString();
if (yearStr.length > 4) {
yearStr = "+" + yearStr.padStart(6, "0");
// TODO: add more low-level helper which combine toString and padStart without extra allocation
var yearStr: string;
var year = this.year;
var isNeg = year < 0;
if (isNeg || year >= 10000) {
yearStr = (isNeg ? "-" : "+") + abs(year).toString().padStart(6, "0");
} else {
yearStr = year.toString().padStart(4, "0");
}

return (
yearStr +
"-" +
month.toString().padStart(2, "0") +
this.month.toString().padStart(2, "0") +
"-" +
day.toString().padStart(2, "0") +
this.day.toString().padStart(2, "0") +
"T" +
this.getUTCHours().toString().padStart(2, "0") +
":" +
Expand Down Expand Up @@ -191,48 +228,54 @@ function epochMillis(
);
}

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

const MILLIS_PER_DAY = 1_000 * 60 * 60 * 24;
const MILLIS_PER_HOUR = 1_000 * 60 * 60;
const MILLIS_PER_MINUTE = 1_000 * 60;
const MILLIS_PER_SECOND = 1_000;

// http://howardhinnant.github.io/date_algorithms.html#is_leap
function isLeap(y: i32): bool {
return y % 4 == 0 && (y % 100 != 0 || y % 400 == 0);
// @ts-ignore: decorator
@inline function euclidRem<T extends number>(a: T, b: T): T {
var m = a % b;
return m + (m < 0 ? b : 0) as T;
}

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

// ymdFromEpochDays returns values via globals to avoid allocations
let year: i32, month: i32, day: i32;
// see: http://howardhinnant.github.io/date_algorithms.html#civil_from_days
function ymdFromEpochDays(z: i32): void {
function ymdFromEpochDays(z: i32): i32 {
z += 719468;
const era = (z >= 0 ? z : z - 146096) / 146097;
const doe = z - era * 146097; // [0, 146096]
const yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
year = yoe + era * 400;
const doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
const mp = (5 * doy + 2) / 153; // [0, 11]
day = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
month = mp + (mp < 10 ? 3 : -9); // [1, 12]
year += (month <= 2 ? 1 : 0);
var era = <u32>floorDiv(z, 146097);
var doe = <u32>z - era * 146097; // [0, 146096]
var yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
var year = yoe + era * 400;
var doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
var mo = (5 * doy + 2) / 153; // [0, 11]
_day = doy - (153 * mo + 2) / 5 + 1; // [1, 31]
mo += mo < 10 ? 3 : -9; // [1, 12]
_month = mo;
year += u32(mo <= 2);
return year;
}

// http://howardhinnant.github.io/date_algorithms.html#days_from_civil
function daysSinceEpoch(y: i32, m: i32, d: i32): i32 {
y -= m <= 2 ? 1 : 0;
const era = (y >= 0 ? y : y - 399) / 400;
const yoe = y - era * 400; // [0, 399]
const doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; // [0, 365]
const doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096]
y -= i32(m <= 2);
var era = <u32>floorDiv(y, 400);
var yoe = <u32>y - era * 400; // [0, 399]
var doy = <u32>(153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; // [0, 365]
var doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096]
return era * 146097 + doe - 719468;
}

// TomohikoSakamoto algorithm from https://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week
function dayOfWeek(year: i32, month: i32, day: i32): i32 {
const tab = memory.data<u8>([0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4]);

year -= i32(month < 3);
year += year / 4 - year / 100 + year / 400;
month = <i32>load<u8>(tab + month - 1);
return euclidRem(year + month + day, 7);
}
5 changes: 4 additions & 1 deletion std/assembly/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1723,7 +1723,9 @@ declare class Date {
): i64;
/** Returns the current UTC timestamp in milliseconds. */
static now(): i64;
static fromString(dateStr: string): Date;
/** Parses a string representation of a date, and returns the number of milliseconds since January 1, 1970, 00:00:00 UTC. */
static parse(dateString: string): Date;
static fromString(dateString: string): Date;
/** Constructs a new date object from an UTC timestamp in milliseconds. */
constructor(value: i64);
/** Returns the UTC timestamp of this date in milliseconds. */
Expand All @@ -1734,6 +1736,7 @@ declare class Date {
getUTCFullYear(): i32;
getUTCMonth(): i32;
getUTCDate(): i32;
getUTCDay(): i32;
getUTCHours(): i32;
getUTCMinutes(): i32;
getUTCSeconds(): i32;
Expand Down
4 changes: 4 additions & 0 deletions std/assembly/util/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,7 @@ export const E_NOT_PINNED: string = "Object is not pinned";
// @ts-ignore: decorator
@lazy @inline
export const E_URI_MALFORMED: string = "URI malformed";

// @ts-ignore: decorator
@lazy @inline
export const E_INVALIDDATE: string = "Invalid Date";
Loading