Skip to content
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

Patch/invariant parse date #1247

Merged
merged 6 commits into from
Jan 11, 2025
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
31 changes: 31 additions & 0 deletions litmus/features/date/invariantParseDate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Calendar, DateField, DateTimeField, MonthField, PureContainer, TimeField } from "cx/widgets";

export default (
<cx>
<PureContainer
controller={{
onInit() {
this.store.set("date", "2024-12-13");
this.store.set("datetime", "2020-02-20");
this.store.set("time", "2024-11-10");
this.store.set("month", "2024-01-01");
this.store.set("calendar", "2022-12-31");
this.store.set("list", "2022-12-31");
},
}}
>
<div style="display: flex; flex-direction: column; gap: 20px">
<DateField value-bind="date" partial />
<DateTimeField value-bind="datetime" partial />
<TimeField value-bind="time" partial />
<MonthField value-bind="month" />

<Calendar value-bind="calendar" startWithMonday={false} />

<TimeField picker="list" value-bind="list" />
<div text-tpl="{date:date}" />
<div text-tpl="{time:time}" />
</div>
</PureContainer>
</cx>
);
5 changes: 3 additions & 2 deletions litmus/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ import Demo from "./features/grid/DockedColumns";
// import Demo from "./features/calendar";
//import Demo from "./features/slider/SliderPreventDefault";
//import Demo from "./features/validator/index";
//import Demo from "./bugs/1075-complex-column-resizing";
//// import Demo from "./bugs/1075-complex-column-resizing";
//import Demo from "./features/grid/RowDndAndGrouping";
//import Demo from "./features/localization/culture-scope";
//import Demo from "./bugs/large_and_small_values_inside_pie_charts_with_gaps";
Expand All @@ -128,7 +128,8 @@ import Demo from "./features/grid/DockedColumns";
// import Demo from "./bugs/TimeAxis";
// import Demo from "./bugs/HighlightedTextSearchBug";
//import Demo from "./bugs/GridScrollUponButtonClickIssue";
import Demo from "./dev/createHotPromiseWindowFactory";
//import Demo from "./dev/createHotPromiseWindowFactory";
import Demo from "./features/date/invariantParseDate";

let store = (window.store = new Store());

Expand Down
35 changes: 18 additions & 17 deletions packages/cx/src/charts/axis/TimeAxis.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { Format } from "../../ui/Format";
import { isNumber } from "../../util/isNumber";
import { zeroTime } from "../../util/date/zeroTime";
import { Console } from "../../util/Console";
import { parseDateInvariant } from "../../util";

Format.registerFactory("yearOrMonth", (format) => {
let year = Format.parse("datetime;yyyy");
let month = Format.parse("datetime;MMM");
return function (date) {
let d = new Date(date);
let d = parseDateInvariant(date);
if (d.getMonth() == 0) return year(d);
else return month(d);
};
Expand All @@ -20,7 +21,7 @@ Format.registerFactory("monthOrDay", (format) => {
let month = Format.parse("datetime;MMM");
let day = Format.parse("datetime;dd");
return function (date) {
let d = new Date(date);
let d = parseDateInvariant(date);
if (d.getDate() == 1) return month(d);
else return day(d);
};
Expand All @@ -34,7 +35,7 @@ export class TimeAxis extends Axis {

if (this.deadZone) {
this.lowerDeadZone = this.deadZone;
pperDeadZone = this.deadZone;
this.upperDeadZone = this.deadZone;
}

this.minLabelDistanceFormatOverride = {
Expand Down Expand Up @@ -145,7 +146,7 @@ function yearNumber(date) {
return monthNumber(date) / 12;
}

const miliSeconds = {
const milliSeconds = {
second: 1000,
minute: 60 * 1000,
hour: 3600 * 1000,
Expand Down Expand Up @@ -203,12 +204,12 @@ class TimeScale {
let v = this.dateCache[date];
if (!v) {
if (this.decode) date = this.decode(date);
v = this.dateCache[date] = Date.parse(date);
v = this.dateCache[date] = parseDateInvariant(date).getTime();
}
return v;

case "number":
return date;
return parseDateInvariant(date).getTime();
}
}

Expand Down Expand Up @@ -339,13 +340,13 @@ class TimeScale {
default:
let minOffset = this.getTimezoneOffset(minDate);
let maxOffset = this.getTimezoneOffset(maxDate);
let mondayOffset = 4 * miliSeconds.day; //new Date(0).getDay() => 4
let mondayOffset = 4 * milliSeconds.day; //new Date(0).getDay() => 4
smin = Math.floor((smin - minOffset - mondayOffset) / tickSize) * tickSize + minOffset + mondayOffset;
smax = Math.ceil((smax - maxOffset - mondayOffset) / tickSize) * tickSize + maxOffset + mondayOffset;
break;

case "month":
tickSize /= miliSeconds.month;
tickSize /= milliSeconds.month;
let minMonth = monthNumber(minDate);
let maxMonth = monthNumber(maxDate);
minMonth = Math.floor(minMonth / tickSize) * tickSize;
Expand All @@ -355,7 +356,7 @@ class TimeScale {
break;

case "year":
tickSize /= miliSeconds.year;
tickSize /= milliSeconds.year;
let minYear = yearNumber(minDate);
let maxYear = yearNumber(maxDate);
minYear = Math.floor(minYear / tickSize) * tickSize;
Expand Down Expand Up @@ -439,13 +440,13 @@ class TimeScale {

let minRange = 1000;

for (let unit in miliSeconds) {
for (let unit in milliSeconds) {
if (!minReached) {
if (unit == this.minTickUnit) minReached = true;
else continue;
}

let unitSize = miliSeconds[unit];
let unitSize = milliSeconds[unit];
let divisions = this.tickDivisions[unit];

if (this.tickSizes.length > 0) {
Expand Down Expand Up @@ -512,13 +513,13 @@ class TimeScale {
break;
}

if (lowerTickUnit && this.minTickUnit && miliSeconds[lowerTickUnit] < miliSeconds[this.minTickUnit])
if (lowerTickUnit && this.minTickUnit && milliSeconds[lowerTickUnit] < milliSeconds[this.minTickUnit])
lowerTickUnit = this.minTickUnit == this.tickMeasure ? null : this.minTickUnit;

if (lowerTickUnit != null && this.scale) {
let bestMinorTickSize = Infinity;
let divisions = this.tickDivisions[lowerTickUnit];
let unitSize = miliSeconds[lowerTickUnit];
let unitSize = milliSeconds[lowerTickUnit];
for (let i = 0; i < divisions.length; i++) {
let divs = divisions[i];
for (let d = 0; d < divs.length; d++) {
Expand Down Expand Up @@ -552,22 +553,22 @@ class TimeScale {
minDate,
maxDate;
if (measure == "year") {
size /= miliSeconds.year;
size /= milliSeconds.year;
minDate = new Date(this.scale.min - this.scale.minPadding);
maxDate = new Date(this.scale.max + this.scale.maxPadding);
start = Math.ceil(yearNumber(minDate) / size) * size;
end = Math.floor(yearNumber(maxDate) / size) * size;
for (let i = start; i <= end; i += size) result.push(new Date(i, 0, 1).getTime());
} else if (measure == "month") {
size /= miliSeconds.month;
size /= milliSeconds.month;
minDate = new Date(this.scale.min - this.scale.minPadding);
maxDate = new Date(this.scale.max + this.scale.maxPadding);
start = Math.ceil(monthNumber(minDate) / size) * size;
end = Math.floor(monthNumber(maxDate) / size) * size;
for (let i = start; i <= end; i += size) result.push(new Date(Math.floor(i / 12), i % 12, 1).getTime());
} else if (measure == "day" || measure == "week") {
let multiplier = measure == "week" ? 7 : 1;
size /= miliSeconds.day;
size /= milliSeconds.day;
minDate = new Date(this.scale.min - this.scale.minPadding);
maxDate = new Date(this.scale.max + this.scale.maxPadding);
let date = zeroTime(minDate);
Expand All @@ -585,7 +586,7 @@ class TimeScale {
}
} else {
let minOffset = this.getTimezoneOffset(new Date(this.scale.min - this.scale.minPadding));
let mondayOffset = 4 * miliSeconds.day;
let mondayOffset = 4 * milliSeconds.day;
let date =
Math.ceil((this.scale.min - this.scale.minPadding - minOffset - mondayOffset) / size) * size +
minOffset +
Expand Down
9 changes: 5 additions & 4 deletions packages/cx/src/ui/Format.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Culture, getCurrentCultureCache } from "./Culture";
import { Format as Fmt, resolveMinMaxFractionDigits, setGetFormatCacheCallback } from "../util/Format";
import { GlobalCacheIdentifier } from "../util/GlobalCacheIdentifier";
import { setGetExpressionCacheCallback } from "../data/Expression";
import { setGetStringTemplateCacheCallback } from "../data/StringTemplate";
import { parseDateInvariant } from "../util";
import { GlobalCacheIdentifier } from "../util/GlobalCacheIdentifier";

export const Format = Fmt;

Expand Down Expand Up @@ -70,19 +71,19 @@ export function enableCultureSensitiveFormatting() {
Fmt.registerFactory(["date", "d"], (fmt, format = "yyyyMMdd") => {
let culture = Culture.getDateTimeCulture();
let formatter = culture.getFormatter(format);
return (value) => formatter.format(new Date(value));
return (value) => formatter.format(parseDateInvariant(value));
});

Fmt.registerFactory(["time", "t"], (fmt, format = "hhmmss") => {
let culture = Culture.getDateTimeCulture();
let formatter = culture.getFormatter(format);
return (value) => formatter.format(new Date(value));
return (value) => formatter.format(parseDateInvariant(value));
});

Fmt.registerFactory(["datetime", "dt"], (fmt, format = "yyyyMd hhmm") => {
let culture = Culture.getDateTimeCulture();
let formatter = culture.getFormatter(format);
return (value) => formatter.format(new Date(value));
return (value) => formatter.format(parseDateInvariant(value));
});

setGetFormatCacheCallback(() => {
Expand Down
5 changes: 3 additions & 2 deletions packages/cx/src/util/Format.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { isNumber } from "../util/isNumber";
import { isUndefined } from "../util/isUndefined";
import { isArray } from "../util/isArray";
import { capitalize } from "./capitalize";
import { parseDateInvariant } from "./date";

//Culture dependent formatters are defined in the ui package.

Expand Down Expand Up @@ -75,14 +76,14 @@ let formatFactory = {

date: function () {
return (value) => {
let date = new Date(value);
let date = parseDateInvariant(value);
return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`;
};
},

time: function () {
return (value) => {
let date = new Date(value);
let date = parseDateInvariant(value);
let h = date.getHours() >= 10 ? date.getHours() : "0" + date.getHours();
let m = date.getMinutes() >= 10 ? date.getMinutes() : "0" + date.getMinutes();
return `${h}:${m}`;
Expand Down
1 change: 1 addition & 0 deletions packages/cx/src/util/date/encodeDate.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export function encodeDate(date: Date): string;
8 changes: 8 additions & 0 deletions packages/cx/src/util/date/encodeDate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
function pad(num) {
const norm = Math.floor(Math.abs(num));
return (norm < 10 ? "0" : "") + norm;
}

export function encodeDate(date) {
return date.getFullYear() + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate());
}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export function encodeDateWithTimezoneOffset(date);
export function encodeDateWithTimezoneOffset(date: Date): string;
20 changes: 11 additions & 9 deletions packages/cx/src/util/date/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
export * from './dateDiff';
export * from './zeroTime';
export * from './monthStart';
export * from './lowerBoundCheck';
export * from './upperBoundCheck';
export * from './maxDate';
export * from './minDate';
export * from './sameDate';
export * from './encodeDateWithTimezoneOffset';
export * from "./dateDiff";
export * from "./zeroTime";
export * from "./monthStart";
export * from "./lowerBoundCheck";
export * from "./upperBoundCheck";
export * from "./maxDate";
export * from "./minDate";
export * from "./sameDate";
export * from "./encodeDateWithTimezoneOffset";
export * from "./encodeDate";
export * from "./parseDateInvariant";
20 changes: 11 additions & 9 deletions packages/cx/src/util/date/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
export * from './dateDiff';
export * from './zeroTime';
export * from './monthStart';
export * from './lowerBoundCheck';
export * from './upperBoundCheck';
export * from './maxDate';
export * from './minDate';
export * from './sameDate';
export * from './encodeDateWithTimezoneOffset';
export * from "./dateDiff";
export * from "./zeroTime";
export * from "./monthStart";
export * from "./lowerBoundCheck";
export * from "./upperBoundCheck";
export * from "./maxDate";
export * from "./minDate";
export * from "./sameDate";
export * from "./encodeDateWithTimezoneOffset";
export * from "./encodeDate";
export * from "./parseDateInvariant";
3 changes: 3 additions & 0 deletions packages/cx/src/util/date/parseDateInvariant.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function parseDateInvariant(input: string | number | Date): Date;

export function overrideParseDateInvariant(newImpl: (input: string | number | Date) => Date): void;
20 changes: 20 additions & 0 deletions packages/cx/src/util/date/parseDateInvariant.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// This module addresses a common issue when handling date strings in the format "yyyy-MM-dd" usually returned by backends.
// In time zones earlier than UTC, creating a Date object from such a string can result in the date being shifted one day earlier.
// This happens because "yyyy-MM-dd" is interpreted as a UTC date at 00:00, and when the browser displays it in local time, it adjusts backward.
// To resolve this, the default implementation (`defaultInvariantParseDate`) appends " 00:00" to the date string,
// explicitly indicating local time. Custom parsing logic can also be registered dynamically using `registerInvariantParseDateImpl`
// to accommodate other formats or requirements.
function defaultParseDateInvariant(input) {
if (typeof input == "string" && input.length == 10 && input[4] == "-" && input[7] == "-")
return new Date(`${input} 00:00`);
return new Date(input);
}
let impl = defaultParseDateInvariant;

export function parseDateInvariant(input) {
return impl(input);
}

export function overrideParseDateInvariant(newImpl) {
impl = newImpl;
}
13 changes: 7 additions & 6 deletions packages/cx/src/widgets/form/Calendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { FocusManager, offFocusOut, oneFocusOut } from "../../ui/FocusManager";
import "../../ui/Format";
import { Localization } from "../../ui/Localization";
import { VDOM, Widget } from "../../ui/Widget";
import { parseDateInvariant } from "../../util";
import { KeyCode } from "../../util/KeyCode";
import { dateDiff } from "../../util/date/dateDiff";
import { lowerBoundCheck } from "../../util/date/lowerBoundCheck";
Expand Down Expand Up @@ -53,17 +54,17 @@ export class Calendar extends Field {
};

if (data.value) {
let d = new Date(data.value);
let d = parseDateInvariant(data.value);
if (!isNaN(d.getTime())) {
data.date = zeroTime(d);
}
}

if (data.refDate) data.refDate = zeroTime(new Date(data.refDate));
if (data.refDate) data.refDate = zeroTime(parseDateInvariant(data.refDate));

if (data.maxValue) data.maxValue = zeroTime(new Date(data.maxValue));
if (data.maxValue) data.maxValue = zeroTime(parseDateInvariant(data.maxValue));

if (data.minValue) data.minValue = zeroTime(new Date(data.minValue));
if (data.minValue) data.minValue = zeroTime(parseDateInvariant(data.minValue));

super.prepareData(...arguments);
}
Expand Down Expand Up @@ -92,7 +93,7 @@ export class Calendar extends Field {
}

if (data.dayData) {
let date = new Date(data.value);
let date = parseDateInvariant(data.value);
let info = data.dayData[date.toDateString()];
if (info && info.disabled) data.error = this.disabledDaysOfWeekErrorText;
}
Expand All @@ -117,7 +118,7 @@ export class Calendar extends Field {
if (this.onBeforeSelect && instance.invoke("onBeforeSelect", e, instance, date) === false) return;

if (widget.partial) {
let mixed = new Date(data.value);
let mixed = parseDateInvariant(data.value);
if (data.value && !isNaN(mixed)) {
mixed.setFullYear(date.getFullYear());
mixed.setMonth(date.getMonth());
Expand Down
Loading