Skip to content

Commit

Permalink
Added caching logic in days util function (#2343)
Browse files Browse the repository at this point in the history
  • Loading branch information
josephmathew900 authored Oct 14, 2024
1 parent 13b138d commit 569da74
Show file tree
Hide file tree
Showing 3 changed files with 282 additions and 48 deletions.
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 };

0 comments on commit 569da74

Please sign in to comment.