mirror of
https://github.com/discourse/discourse.git
synced 2025-05-24 14:12:10 +08:00
REFACTOR: local dates to improve reliability with DST and recurrence (#9379)
This commit improves testing and separates local-date generation from dates with zone manipulations.
This commit is contained in:
@ -0,0 +1,243 @@
|
||||
import DateWithZoneHelper from "./date-with-zone-helper";
|
||||
|
||||
const TIME_FORMAT = "LLL";
|
||||
const DATE_FORMAT = "LL";
|
||||
const RANGE_SEPARATOR = "→";
|
||||
|
||||
export default class LocalDateBuilder {
|
||||
constructor(params = {}, localTimezone) {
|
||||
this.time = params.time;
|
||||
this.date = params.date;
|
||||
this.recurring = params.recurring;
|
||||
this.timezones = Array.from(
|
||||
new Set((params.timezones || []).filter(Boolean))
|
||||
);
|
||||
this.timezone = params.timezone || "UTC";
|
||||
this.calendar =
|
||||
typeof params.calendar === "undefined" ? true : params.calendar;
|
||||
this.displayedTimezone = params.displayedTimezone;
|
||||
this.format = params.format || (this.time ? TIME_FORMAT : DATE_FORMAT);
|
||||
this.countdown = params.countdown;
|
||||
this.localTimezone = localTimezone;
|
||||
}
|
||||
|
||||
build() {
|
||||
const [year, month, day] = this.date.split("-").map(x => parseInt(x, 10));
|
||||
const [hour, minute] = (this.time || "")
|
||||
.split(":")
|
||||
.map(x => (x ? parseInt(x, 10) : undefined));
|
||||
|
||||
let displayedTimezone;
|
||||
if (this.time) {
|
||||
displayedTimezone = this.displayedTimezone || this.localTimezone;
|
||||
} else {
|
||||
displayedTimezone =
|
||||
this.displayedTimezone || this.localTimezone || this.timezone;
|
||||
}
|
||||
|
||||
let localDate = new DateWithZoneHelper({
|
||||
year,
|
||||
month: month ? month - 1 : null,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
timezone: this.timezone,
|
||||
localTimezone: this.localTimezone
|
||||
});
|
||||
|
||||
if (this.recurring) {
|
||||
const [count, type] = this.recurring.split(".");
|
||||
|
||||
const repetitionsForType = localDate.repetitionsBetweenDates(
|
||||
this.recurring,
|
||||
moment.tz(this.localTimezone)
|
||||
);
|
||||
|
||||
localDate = localDate.add(repetitionsForType + parseInt(count, 10), type);
|
||||
}
|
||||
|
||||
const previews = this._generatePreviews(localDate, displayedTimezone);
|
||||
|
||||
return {
|
||||
pastEvent:
|
||||
!this.recurring &&
|
||||
moment.tz(this.localTimezone).isAfter(localDate.datetime),
|
||||
formated: this._applyFormatting(localDate, displayedTimezone),
|
||||
previews,
|
||||
textPreview: this._generateTextPreviews(previews)
|
||||
};
|
||||
}
|
||||
|
||||
_generateTextPreviews(previews) {
|
||||
return previews
|
||||
.map(preview => {
|
||||
const formatedZone = this._zoneWithoutPrefix(preview.timezone);
|
||||
return `${formatedZone} ${preview.formated}`;
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
_generatePreviews(localDate, displayedTimezone) {
|
||||
const previewedTimezones = [];
|
||||
|
||||
const timezones = this.timezones.filter(
|
||||
timezone =>
|
||||
!this._isEqualZones(timezone, this.localTimezone) &&
|
||||
!this._isEqualZones(timezone, this.timezone)
|
||||
);
|
||||
|
||||
previewedTimezones.push({
|
||||
timezone: this.localTimezone,
|
||||
current: true,
|
||||
formated: this._createDateTimeRange(localDate, this.time)
|
||||
});
|
||||
|
||||
if (
|
||||
this.timezone &&
|
||||
displayedTimezone === this.localTimezone &&
|
||||
this.timezone !== displayedTimezone &&
|
||||
!this._isEqualZones(displayedTimezone, this.timezone)
|
||||
) {
|
||||
timezones.unshift(this.timezone);
|
||||
}
|
||||
|
||||
timezones.forEach(timezone => {
|
||||
if (this._isEqualZones(timezone, displayedTimezone)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._isEqualZones(timezone, this.localTimezone)) {
|
||||
timezone = this.localTimezone;
|
||||
}
|
||||
|
||||
previewedTimezones.push({
|
||||
timezone,
|
||||
formated: this._createDateTimeRange(
|
||||
localDate.datetimeWithZone(timezone),
|
||||
this.time
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
if (!previewedTimezones.length) {
|
||||
previewedTimezones.push({
|
||||
timezone: "Etc/UTC",
|
||||
formated: this._createDateTimeRange(
|
||||
localDate.datetimeWithZone("Etc/UTC"),
|
||||
this.time
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
return previewedTimezones.uniqBy("timezone");
|
||||
}
|
||||
|
||||
_isEqualZones(timezoneA, timezoneB) {
|
||||
if ((timezoneA || timezoneB) && (!timezoneA || !timezoneB)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (timezoneA.includes(timezoneB) || timezoneB.includes(timezoneA)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
moment.tz(timezoneA).utcOffset() === moment.tz(timezoneB).utcOffset()
|
||||
);
|
||||
}
|
||||
|
||||
_createDateTimeRange(startRange, time) {
|
||||
// if a time has been given we do not attempt to automatically create a range
|
||||
// instead we show only one date with a format showing the time
|
||||
if (time) {
|
||||
return startRange.format(TIME_FORMAT);
|
||||
} else {
|
||||
const endRange = startRange.add(24, "hours");
|
||||
return [
|
||||
startRange.format(this.format || "LLLL"),
|
||||
RANGE_SEPARATOR,
|
||||
endRange.format(this.format || "LLLL")
|
||||
].join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
_applyFormatting(localDate, displayedTimezone) {
|
||||
if (this.countdown) {
|
||||
const diffTime = moment.tz(this.localTimezone).diff(localDate.datetime);
|
||||
|
||||
if (diffTime < 0) {
|
||||
return moment.duration(diffTime).humanize();
|
||||
} else {
|
||||
return I18n.t("discourse_local_dates.relative_dates.countdown.passed");
|
||||
}
|
||||
}
|
||||
|
||||
const sameTimezone = this._isEqualZones(
|
||||
displayedTimezone,
|
||||
this.localTimezone
|
||||
);
|
||||
|
||||
if (this.calendar) {
|
||||
const inCalendarRange = moment
|
||||
.tz(this.localTimezone)
|
||||
.isBetween(
|
||||
localDate.subtract(2, "day").datetime,
|
||||
localDate.add(1, "day").datetime.endOf("day")
|
||||
);
|
||||
|
||||
if (inCalendarRange && sameTimezone) {
|
||||
return localDate.datetime.calendar(
|
||||
moment.tz(this.localTimezone),
|
||||
this._calendarFormats(this.time ? this.time : null)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!sameTimezone) {
|
||||
return this._formatWithZone(localDate, displayedTimezone, this.format);
|
||||
}
|
||||
|
||||
return localDate.format(this.format);
|
||||
}
|
||||
|
||||
_calendarFormats(time) {
|
||||
return {
|
||||
sameDay: this._translateCalendarKey(time, "today"),
|
||||
nextDay: this._translateCalendarKey(time, "tomorrow"),
|
||||
lastDay: this._translateCalendarKey(time, "yesterday"),
|
||||
sameElse: "L"
|
||||
};
|
||||
}
|
||||
|
||||
_translateCalendarKey(time, key) {
|
||||
const translated = I18n.t(`discourse_local_dates.relative_dates.${key}`, {
|
||||
time: "LT"
|
||||
});
|
||||
|
||||
if (time) {
|
||||
return translated
|
||||
.split("LT")
|
||||
.map(w => `[${w}]`)
|
||||
.join("LT");
|
||||
} else {
|
||||
return `[${translated.replace(" LT", "")}]`;
|
||||
}
|
||||
}
|
||||
|
||||
_formatTimezone(timezone) {
|
||||
return timezone
|
||||
.replace("_", " ")
|
||||
.replace("Etc/", "")
|
||||
.split("/");
|
||||
}
|
||||
|
||||
_zoneWithoutPrefix(timezone) {
|
||||
const [part1, part2] = this._formatTimezone(timezone);
|
||||
return part2 || part1;
|
||||
}
|
||||
|
||||
_formatWithZone(localDate, displayedTimezone, format) {
|
||||
let formated = localDate.datetimeWithZone(displayedTimezone).format(format);
|
||||
return `${formated} (${this._zoneWithoutPrefix(displayedTimezone)})`;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user