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

Added caching logic in days util function #2343

Merged
merged 1 commit into from
Oct 14, 2024
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
96 changes: 96 additions & 0 deletions src/utils/dayjs/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import pureDayjs from "dayjs";
import localeData from "dayjs/plugin/localeData";
import utc from "dayjs/plugin/utc";
import weekday from "dayjs/plugin/weekday";
import weekOfYear from "dayjs/plugin/weekOfYear";

import timezone from "./timezonePlugin";

pureDayjs.extend(weekOfYear);
pureDayjs.extend(weekday);
pureDayjs.extend(localeData);
pureDayjs.extend(utc);
pureDayjs.extend(timezone);

class LRUCache {
constructor(limit) {
this.limit = limit;
this.cache = new Map();
}

get(key) {
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);

return value;
}

set(key, value) {
if (this.cache.size >= this.limit) {
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
this.cache.set(key, value);
}

has(key) {
return this.cache.has(key);
}
}

const hasTimezone = dateString => {
const timezoneRegex = /Z|[+-]\d{2}:\d{2}$|[+-]\d{4}$|GMT([+-]\d{4})?$/;

return timezoneRegex.test(dateString);
};

const cache = new LRUCache(1000);

const dayjs = (...args) => {
const cacheKey = JSON.stringify(args);

if (cache.has(cacheKey) && args[0] !== undefined) return cache.get(cacheKey);

const userTimezone = pureDayjs.tz().$x.$timezone;
const browserTimezone = pureDayjs.tz.guess();
const timezone = userTimezone || browserTimezone;

if (userTimezone === browserTimezone || userTimezone === undefined) {
const result = pureDayjs(...args);

if (args[0] !== undefined) cache.set(cacheKey, result);

return result;
}

if (args.length > 0 && (typeof args[0] === "string" || args[0] === null)) {
const pureDayjsArgs = args.slice(0, Math.min(args.length, 2));

if (hasTimezone(args[0])) {
args[0] = pureDayjs(...pureDayjsArgs);
} else {
args[0] = pureDayjs(...pureDayjsArgs).format("YYYY-MM-DD HH:mm:ss");
args[1] = "YYYY-MM-DD HH:mm:ss";
}
}

if (args[0]?.toString() === "Invalid Date") {
const result = pureDayjs(...args);

if (args[0] !== undefined) cache.set(cacheKey, result);

return result;
}

const result =
args.length === 2 ? pureDayjs.tz(...args, timezone) : pureDayjs.tz(...args);

if (args[0] !== undefined) cache.set(cacheKey, result);

return result;
};

Object.assign(dayjs, { ...pureDayjs });

export default dayjs;
183 changes: 183 additions & 0 deletions src/utils/dayjs/timezonePlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
const typeToPos = {
year: 0,
month: 1,
day: 2,
hour: 3,
minute: 4,
second: 5,
};

const dateTimeFormatDefaults = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
};

// Cache time-zone lookups from Intl.DateTimeFormat,
// as it is a *very* slow method.
const dtfCache = {};
const getDateTimeFormat = (timezone, options = {}) => {
const timeZoneName = options.timeZoneName || "short";
const key = `${timezone}|${timeZoneName}`;
let dtf = dtfCache[key];
if (!dtf) {
dtf = new Intl.DateTimeFormat("en-US", {
...dateTimeFormatDefaults,
hour12: false,
timeZone: timezone,
timeZoneName,
});
dtfCache[key] = dtf;
}

return dtf;
};

const localeStringifierCache = {};
const getLocaleStringifier = timezone => {
let localeStringifier = localeStringifierCache[timezone];
if (!localeStringifier) {
localeStringifier = new Intl.DateTimeFormat("en-US", {
...dateTimeFormatDefaults,
timeZone: timezone,
});
localeStringifierCache[timezone] = localeStringifier;
}

return localeStringifier;
};

// eslint-disable-next-line no-unused-vars
export default (o, c, d) => {
let defaultTimezone;

const makeFormatParts = (timestamp, timezone, options = {}) => {
const date = new Date(timestamp);
const dtf = getDateTimeFormat(timezone, options);

return dtf.formatToParts(date);
};

const tzOffset = (timestamp, timezone) => {
const formatResult = makeFormatParts(timestamp, timezone);
const filled = [];
for (let i = 0; i < formatResult.length; i += 1) {
const { type, value } = formatResult[i];
const pos = typeToPos[type];

if (pos >= 0) {
filled[pos] = parseInt(value, 10);
}
}
const hour = filled[3];
// Workaround for the same behavior in different node version
// https://github.com/nodejs/node/issues/33027
/* istanbul ignore next */
const fixedHour = hour === 24 ? 0 : hour;
const utcString = `${filled[0]}-${filled[1]}-${filled[2]} ${fixedHour}:${filled[4]}:${filled[5]}:000`;
const utcTs = d.utc(utcString).valueOf();
let asTS = Number(timestamp);
const over = asTS % 1000;
asTS -= over;

return (utcTs - asTS) / (60 * 1000);
};

// find the right offset a given local time. The o input is our guess, which determines which
// offset we'll pick in ambiguous cases (e.g. there are two 3 AMs b/c Fallback DST)
// https://github.com/moment/luxon/blob/master/src/datetime.js#L76
const fixOffset = (localTS, o0, tz) => {
// Our UTC time is just a guess because our offset is just a guess
let utcGuess = localTS - o0 * 60 * 1000;
// Test whether the zone matches the offset for this ts
const o2 = tzOffset(utcGuess, tz);
// If so, offset didn't change and we're done
if (o0 === o2) {
return [utcGuess, o0];
}
// If not, change the ts by the difference in the offset
utcGuess -= (o2 - o0) * 60 * 1000;
// If that gives us the local time we want, we're done
const o3 = tzOffset(utcGuess, tz);
if (o2 === o3) {
return [utcGuess, o2];
}

// If it's different, we're in a hole time.
// The offset has changed, but the we don't adjust the time
return [localTS - Math.min(o2, o3) * 60 * 1000, Math.max(o2, o3)];
};

const proto = c.prototype;

// eslint-disable-next-line default-param-last
proto.tz = function (timezone = defaultTimezone, keepLocalTime) {
const oldOffset = this.utcOffset();
const date = this.toDate();
const target = getLocaleStringifier(timezone).format(date);
const diff = Math.round((date - new Date(target)) / 1000 / 60);
let ins = d(target)
.$set("millisecond", this.$ms)
.utcOffset(-Math.round(date.getTimezoneOffset() / 15) * 15 - diff, true);
if (keepLocalTime) {
const newOffset = ins.utcOffset();
ins = ins.add(oldOffset - newOffset, "minute");
}
ins.$x.$timezone = timezone;

return ins;
};

proto.offsetName = function (type) {
// type: short(default) / long
const zone = this.$x.$timezone || d.tz.guess();
const result = makeFormatParts(this.valueOf(), zone, {
timeZoneName: type,
}).find(m => m.type.toLowerCase() === "timezonename");

return result && result.value;
};

const oldStartOf = proto.startOf;
proto.startOf = function (units, startOf) {
if (!this.$x || !this.$x.$timezone) {
return oldStartOf.call(this, units, startOf);
}

const withoutTz = d(this.format("YYYY-MM-DD HH:mm:ss:SSS"));
const startOfWithoutTz = oldStartOf.call(withoutTz, units, startOf);

return startOfWithoutTz.tz(this.$x.$timezone, true);
};

d.tz = function (input, arg1, arg2) {
const parseFormat = arg2 && arg1;
const timezone = arg2 || arg1 || defaultTimezone;
const previousOffset = tzOffset(Number(d()), timezone);
if (typeof input !== "string") {
// timestamp number || js Date || Day.js
return d(input).tz(timezone);
}
const localTs = d.utc(input, parseFormat).valueOf();
const [targetTs, targetOffset] = fixOffset(
localTs,
previousOffset,
timezone
);
const ins = d(targetTs).utcOffset(targetOffset);
ins.$x.$timezone = timezone;

return ins;
};

d.tz.guess = function () {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
};

d.tz.setDefault = function (timezone) {
defaultTimezone = timezone;
};
};
51 changes: 3 additions & 48 deletions src/utils/index.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
import pureDayjs from "dayjs";
import localeData from "dayjs/plugin/localeData";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import weekday from "dayjs/plugin/weekday";
import weekOfYear from "dayjs/plugin/weekOfYear";
import { preprocessForSerialization } from "neetocist";
import { parse, stringify } from "qs";
import { complement, equals, isEmpty, omit, pipe, toPairs } from "ramda";

// eslint-disable-next-line import/extensions
import en from "src/translations/en.json";

pureDayjs.extend(weekOfYear);
pureDayjs.extend(weekday);
pureDayjs.extend(localeData);
pureDayjs.extend(utc);
pureDayjs.extend(timezone);
import dayjs from "./dayjs";

const getEnTranslationValue = translationKey =>
translationKey.split(".").reduce((acc, key) => acc[key], en);
Expand All @@ -33,43 +23,6 @@ const getScrollbarWidth = () => {
return scrollbarWidth;
};

const hasTimezone = dateString => {
const timezoneRegex = /Z|[+-]\d{2}:\d{2}$|GMT([+-]\d{4})?$/;

return timezoneRegex.test(dateString);
};

// eslint-disable-next-line import/exports-last
export const dayjs = (...args) => {
if (
pureDayjs.tz().$x.$timezone === pureDayjs.tz.guess() ||
pureDayjs.tz().$x.$timezone === undefined
) {
return pureDayjs(...args);
}

if (args.length > 0 && typeof args[0] === "string") {
const pureDayjsArgs = args.slice(0, Math.min(args.length, 2));

if (hasTimezone(args[0])) {
args[0] = pureDayjs(...pureDayjsArgs);
} else {
args[0] = pureDayjs(...pureDayjsArgs).format("YYYY-MM-DD HH:mm:ss");
args[1] = "YYYY-MM-DD HH:mm:ss";
}
}

if (args[0]?.toString() === "Invalid Date") return pureDayjs(...args);

const timezone = pureDayjs.tz().$x.$timezone || pureDayjs.tz.guess();

return args.length === 2
? pureDayjs.tz(...args, timezone)
: pureDayjs.tz(...args);
};

Object.assign(dayjs, { ...pureDayjs });

export const getTimezoneAppliedDateTime = inputDateTime => {
if (!inputDateTime) return null;

Expand Down Expand Up @@ -233,3 +186,5 @@ export const setToLocalStorage = (key, value) =>

// eslint-disable-next-line @bigbinary/neeto/no-local-storage
export const removeFromLocalStorage = key => localStorage.removeItem(key);

export { dayjs };
Loading