This commit is contained in:
2025-11-03 12:24:01 +02:00
commit 0806865287
177 changed files with 18453 additions and 0 deletions

15
web/util/package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "@greatness/util",
"version": "1.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"test": "jest"
},
"dependencies": {
"dayjs": "1.11.13"
},
"devDependencies": {
"jest": "30.0.5"
}
}

View File

@@ -0,0 +1,43 @@
import ArrayUtil from "./ArrayUtil";
describe("ArrayUtil", () => {
it("isEmpty", () => {
expect(ArrayUtil.isEmpty()).toBe(true);
expect(ArrayUtil.isEmpty(null)).toBe(true);
expect(ArrayUtil.isEmpty([])).toBe(true);
expect(ArrayUtil.isEmpty([undefined])).toBe(false);
});
it("removeDuplicates", () => {
expect(
ArrayUtil.removeDuplicates(
[
{ val: "a" },
{ val: "a" },
{ val: "b" },
{ val: "b " },
{ val: "c" },
{ val: "c" },
{ val: "d" },
],
"val",
),
).toEqual([
{ val: "a" },
{ val: "b" },
{ val: "b " },
{ val: "c" },
{ val: "d" },
]);
});
it("uniqueStrings", () => {
expect(ArrayUtil.uniqueStrings(["A@a.ee", "a@A.ee", "a@a.ee"])).toEqual([
"a@a.ee",
]);
expect(
ArrayUtil.uniqueStrings(["A.B@a.ee", "a.b@A.ee", "a.b@a.ee", "b.c@a.ee"]),
).toEqual(["a.b@a.ee", "b.c@a.ee"]);
});
});

21
web/util/src/ArrayUtil.ts Normal file
View File

@@ -0,0 +1,21 @@
export default class ArrayUtil {
public static isEmpty(array?: unknown[] | null): boolean {
return !Array.isArray(array) || array.length === 0;
}
public static removeDuplicates<T>(array: T[], key: keyof T): T[] {
const seen = new Set();
return array.filter((item) => {
const value = item[key];
if (seen.has(value)) {
return false;
}
seen.add(value);
return true;
});
}
public static uniqueStrings(array: string[]): string[] {
return [...new Set(array.map((value) => value.toLowerCase()))];
}
}

View File

@@ -0,0 +1,42 @@
import BlobUtil from "./BlobUtil";
describe("BlobUtil", () => {
describe("blobToBase64", () => {
test("converts blob to base64 string", async () => {
const testString = "Hello, World!";
const blob = new Blob([testString], { type: "text/plain" });
const result = await BlobUtil.blobToBase64(blob);
expect(result).toBe("SGVsbG8sIFdvcmxkIQ==");
});
test("converts empty blob to empty base64 string", async () => {
const blob = new Blob([], { type: "text/plain" });
const result = await BlobUtil.blobToBase64(blob);
expect(result).toBe("");
});
test("converts binary data blob to base64", async () => {
const binaryData = new Uint8Array([72, 101, 108, 108, 111]); // "Hello" in ASCII
const blob = new Blob([binaryData], { type: "application/octet-stream" });
const result = await BlobUtil.blobToBase64(blob);
expect(result).toBe("SGVsbG8=");
});
test("handles blob with special characters", async () => {
const testString = "Testing 123 @#$%";
const blob = new Blob([testString], { type: "text/plain" });
const result = await BlobUtil.blobToBase64(blob);
// Convert back to verify
const decoded = Buffer.from(result, "base64").toString();
expect(decoded).toBe(testString);
});
});
});

6
web/util/src/BlobUtil.ts Normal file
View File

@@ -0,0 +1,6 @@
export default class BlobUtil {
public static async blobToBase64(blob: Blob): Promise<string> {
const blobBuffer = Buffer.from(await blob.arrayBuffer());
return blobBuffer.toString("base64");
}
}

View File

@@ -0,0 +1,21 @@
import BooleanUtil from "./BooleanUtil";
describe("BooleanUtil", () => {
it("isTrue", () => {
expect(BooleanUtil.isTrue("true")).toEqual(true);
expect(BooleanUtil.isTrue(true)).toEqual(true);
expect(BooleanUtil.isTrue("")).toEqual(false);
expect(BooleanUtil.isTrue()).toEqual(false);
expect(BooleanUtil.isTrue("false")).toEqual(false);
});
it("isFalse", () => {
expect(BooleanUtil.isFalse("false")).toEqual(true);
expect(BooleanUtil.isFalse(false)).toEqual(true);
expect(BooleanUtil.isFalse("")).toEqual(false);
expect(BooleanUtil.isFalse()).toEqual(false);
expect(BooleanUtil.isFalse("true")).toEqual(false);
});
});

View File

@@ -0,0 +1,9 @@
export default class BooleanUtil {
public static isTrue(value?: unknown): boolean {
return value === true || value === "true";
}
public static isFalse(value?: unknown): boolean {
return value === false || value === "false";
}
}

View File

@@ -0,0 +1,179 @@
import CommonUtil from "./CommonUtil";
describe("Util", () => {
describe("query", () => {
test("returns empty string when empty object is passed", () => {
const result = CommonUtil.query({});
expect(result).toEqual("");
});
test("returns encoded query string with single key-value pair", () => {
const result = CommonUtil.query({ key: "value" });
expect(result).toEqual("key=value");
});
test("returns encoded query string with multiple key-value pairs", () => {
const result = CommonUtil.query({ key1: "value1", key2: "value2" });
expect(result).toEqual("key1=value1&key2=value2");
});
test("ignores key-value pairs with undefined values", () => {
const result = CommonUtil.query({ key1: "value1", key2: undefined });
expect(result).toEqual("key1=value1");
});
test("ignores key-value pairs with undefined keys", () => {
const result = CommonUtil.query({ key1: "value1", undefined: "value2" });
expect(result).toEqual("key1=value1");
});
});
describe("sleep", () => {
test("resolves after the specified time", async () => {
const start = new Date();
await CommonUtil.sleep(200);
const end = new Date();
const duration = end.getTime() - start.getTime();
expect(duration).toBeGreaterThan(198);
});
});
describe("sleepRandom", () => {
test("resolves after the specified time", async () => {
const start = new Date();
await CommonUtil.sleepRandom(300);
const end = new Date();
const duration = end.getTime() - start.getTime();
expect(duration).toBeGreaterThan(299);
});
});
describe("minutesToMs", () => {
test("converts minutes to milliseconds", () => {
const result = CommonUtil.minutesToMs(2);
expect(result).toEqual(120000);
});
});
describe("between", () => {
test("returns true when number is between min and max", () => {
expect(CommonUtil.between(5, 1, 10)).toBe(true);
});
test("returns true when number equals min", () => {
expect(CommonUtil.between(5, 5, 10)).toBe(true);
});
test("returns true when number equals max", () => {
expect(CommonUtil.between(10, 5, 10)).toBe(true);
});
test("returns false when number is below min", () => {
expect(CommonUtil.between(1, 5, 10)).toBe(false);
});
test("returns false when number is above max", () => {
expect(CommonUtil.between(15, 5, 10)).toBe(false);
});
});
describe("futureDateByHours", () => {
test("returns date in future by specified hours", () => {
const now = new Date();
const futureDate = CommonUtil.futureDateByHours(2);
expect(futureDate.getTime()).toBeGreaterThan(now.getTime());
expect(futureDate.getHours()).toBe((now.getHours() + 2) % 24);
});
});
describe("futureDateByMinutes", () => {
test("returns date in future by specified minutes", () => {
const now = new Date();
const futureDate = CommonUtil.futureDateByMinutes(30);
expect(futureDate.getTime()).toBeGreaterThan(now.getTime());
const expectedTime = now.getTime() + (30 + 1) * 60 * 1000; // +1 for padding
expect(Math.abs(futureDate.getTime() - expectedTime)).toBeLessThan(1000);
});
});
describe("futureDateBySeconds", () => {
test("returns date in future by specified seconds", () => {
const now = new Date();
const futureDate = CommonUtil.futureDateBySeconds(30);
expect(futureDate.getTime()).toBeGreaterThan(now.getTime());
const expectedTime = now.getTime() + 30 * 1000;
expect(Math.abs(futureDate.getTime() - expectedTime)).toBeLessThan(1000);
});
});
describe("hoursToMinutes", () => {
test("converts hours to minutes", () => {
expect(CommonUtil.hoursToMinutes(2)).toBe(120);
expect(CommonUtil.hoursToMinutes(0.5)).toBe(30);
expect(CommonUtil.hoursToMinutes(24)).toBe(1440);
});
});
describe("minutesToSeconds", () => {
test("converts minutes to seconds", () => {
expect(CommonUtil.minutesToSeconds(1)).toBe(60);
expect(CommonUtil.minutesToSeconds(5)).toBe(300);
expect(CommonUtil.minutesToSeconds(0.5)).toBe(30);
});
});
describe("random", () => {
test("returns number within specified range", () => {
for (let i = 0; i < 100; i++) {
const result = CommonUtil.random(1, 10);
expect(result).toBeGreaterThanOrEqual(1);
expect(result).toBeLessThanOrEqual(10);
}
});
test("returns single number when min equals max", () => {
expect(CommonUtil.random(5, 5)).toBe(5);
});
test("handles negative numbers", () => {
for (let i = 0; i < 10; i++) {
const result = CommonUtil.random(-10, -1);
expect(result).toBeGreaterThanOrEqual(-10);
expect(result).toBeLessThanOrEqual(-1);
}
});
});
describe("query - extended tests", () => {
test("handles boolean values", () => {
const result = CommonUtil.query({ active: true, visible: false });
expect(result).toBe("active=true&visible=false");
});
test("handles number values", () => {
const result = CommonUtil.query({ count: 42, rating: 4.5 });
expect(result).toBe("count=42&rating=4.5");
});
test("handles special characters in values", () => {
const result = CommonUtil.query({
search: "hello world",
tag: "tag&value",
});
expect(result).toBe("search=hello%20world&tag=tag%26value");
});
test("ignores keys that are string 'undefined'", () => {
const result = CommonUtil.query({ key1: "value1", undefined: "value2" });
expect(result).toBe("key1=value1");
});
test("handles zero values", () => {
const result = CommonUtil.query({ count: 0, rating: 0 });
expect(result).toBe("count=0&rating=0");
});
});
});

View File

@@ -0,0 +1,58 @@
export type QueryParams = Record<string, string | number | boolean | undefined>;
const CommonUtil = {
between(x: number, min: number, max: number) {
return x >= min && x <= max;
},
futureDateByHours(hours: number): Date {
return this.futureDateByMinutes(this.hoursToMinutes(hours));
},
futureDateByMinutes(minutes: number): Date {
const MINUTES_EXTRA_PADDING = 1;
const minutesInFuture = minutes + MINUTES_EXTRA_PADDING;
return new Date(
new Date().setMinutes(new Date().getMinutes() + minutesInFuture),
);
},
futureDateBySeconds(secondsInFuture: number) {
return new Date(
new Date().setSeconds(new Date().getSeconds() + secondsInFuture),
);
},
hoursToMinutes(hours: number) {
return hours * 60;
},
minutesToMs: (minutes: number) => minutes * 60_000,
minutesToSeconds(minutes: number) {
return minutes * 60;
},
query: (params: QueryParams) =>
Object.entries(params)
.map(([paramKey, paramValue]) => {
if (
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
paramKey === undefined ||
paramKey === "undefined" ||
paramValue === undefined
) {
return null;
}
const [key, value] = [
encodeURIComponent(paramKey),
encodeURIComponent(paramValue),
];
return `${key}=${value}`;
})
.filter((value) => value !== null)
.join("&"),
random: (min: number, max: number) =>
Math.floor(Math.random() * (max - min + 1) + min),
async sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
},
async sleepRandom(ms: number) {
return this.sleep(this.random(ms, ms + ms * 0.2));
},
};
export default CommonUtil;

View File

@@ -0,0 +1,191 @@
import DateFormat from "./DateFormat";
describe("DateFormat", () => {
describe("toIso8601DateString", () => {
it("should format Date object to ISO 8601 date string", () => {
expect(DateFormat.toIso8601DateString(new Date(2024, 9, 15))).toEqual(
"2024-10-15",
);
});
it("should format string date to ISO 8601 date string", () => {
expect(DateFormat.toIso8601DateString("2024-10-15")).toEqual(
"2024-10-15",
);
});
it("should format with custom format", () => {
expect(
DateFormat.toIso8601DateString(
new Date(2024, 9, 15),
"YYYY-MM-DD HH:mm:ss",
),
).toMatch(/2024-10-15 \d{2}:\d{2}:\d{2}/);
});
it("should handle leap year dates", () => {
expect(DateFormat.toIso8601DateString(new Date(2024, 1, 29))).toEqual(
"2024-02-29",
);
});
});
describe("toDayjs", () => {
it("should convert ISO string with timezone to dayjs", () => {
expect(
DateFormat.toDayjs("2024-10-10T12:00:00+02:00").toDate().toISOString(),
).toEqual("2024-10-10T10:00:00.000Z");
});
it("should convert date string to dayjs with Europe/Tallinn timezone", () => {
expect(DateFormat.toDayjs("2024-10-10").toDate().toISOString()).toEqual(
"2024-10-09T21:00:00.000Z",
);
});
it("should handle Date objects", () => {
const date = new Date(2024, 9, 15, 10, 30, 0);
const result = DateFormat.toDayjs(date);
expect(result.format("YYYY-MM-DD")).toEqual("2024-10-15");
});
it("should handle custom format parsing", () => {
const result = DateFormat.toDayjs("15/10/2024", "DD/MM/YYYY");
expect(result.format("YYYY-MM-DD")).toEqual("2024-10-15");
});
it("should handle undefined input", () => {
const result = DateFormat.toDayjs();
expect(result.isValid()).toBe(true);
});
it("should handle edge of day transitions", () => {
const result = DateFormat.toDayjs("2024-10-10T00:00:00");
expect(result.format("YYYY-MM-DD HH:mm:ss")).toEqual(
"2024-10-10 00:00:00",
);
});
});
describe("toIso8601DateWithZeroHours", () => {
it("should format date with zero hours", () => {
const date = new Date(2024, 9, 15, 14, 30, 45);
expect(DateFormat.toIso8601DateWithZeroHours(date)).toEqual(
"2024-10-15T00:00:00",
);
});
it("should handle midnight date", () => {
const date = new Date(2024, 9, 15, 0, 0, 0);
expect(DateFormat.toIso8601DateWithZeroHours(date)).toEqual(
"2024-10-15T00:00:00",
);
});
it("should handle end of day", () => {
const date = new Date(2024, 9, 15, 23, 59, 59);
expect(DateFormat.toIso8601DateWithZeroHours(date)).toEqual(
"2024-10-15T00:00:00",
);
});
it("should handle leap year date", () => {
const date = new Date(2024, 1, 29, 12, 0, 0);
expect(DateFormat.toIso8601DateWithZeroHours(date)).toEqual(
"2024-02-29T00:00:00",
);
});
});
describe("toIso8601DateWithZeroMinutes", () => {
it("should format date with zero minutes", () => {
const date = new Date(2024, 9, 15, 14, 30, 45);
expect(DateFormat.toIso8601DateWithZeroMinutes(date)).toEqual(
"2024-10-15T14:00:00",
);
});
it("should handle midnight hour", () => {
const date = new Date(2024, 9, 15, 0, 30, 45);
expect(DateFormat.toIso8601DateWithZeroMinutes(date)).toEqual(
"2024-10-15T00:00:00",
);
});
it("should handle noon", () => {
const date = new Date(2024, 9, 15, 12, 45, 30);
expect(DateFormat.toIso8601DateWithZeroMinutes(date)).toEqual(
"2024-10-15T12:00:00",
);
});
it("should handle end of day hour", () => {
const date = new Date(2024, 9, 15, 23, 59, 59);
expect(DateFormat.toIso8601DateWithZeroMinutes(date)).toEqual(
"2024-10-15T23:00:00",
);
});
it("should handle single digit hour", () => {
const date = new Date(2024, 9, 15, 5, 30, 45);
expect(DateFormat.toIso8601DateWithZeroMinutes(date)).toEqual(
"2024-10-15T05:00:00",
);
});
});
describe("timezone handling", () => {
it("should handle Europe/Tallinn timezone correctly", () => {
// Test winter time (EET = UTC+2)
const winterDate = DateFormat.toDayjs("2024-01-15T12:00:00");
expect(winterDate.utc().format("HH:mm")).toEqual("10:00");
// Test summer time (EEST = UTC+3)
const summerDate = DateFormat.toDayjs("2024-07-15T12:00:00");
expect(summerDate.utc().format("HH:mm")).toEqual("09:00");
});
it("should maintain timezone consistency across different methods", () => {
const date = new Date(2024, 6, 15, 12, 30, 45); // July 15, 2024
const dayjsResult = DateFormat.toDayjs(date);
const isoResult = DateFormat.toIso8601DateString(date);
expect(dayjsResult.format("YYYY-MM-DD")).toEqual(isoResult);
});
});
describe("edge cases", () => {
it("should handle invalid date strings gracefully", () => {
const result = DateFormat.toDayjs("invalid-date");
expect(result.isValid()).toBe(false);
});
it("should handle year boundaries", () => {
const newYear = new Date(2024, 0, 1, 0, 0, 0);
expect(DateFormat.toIso8601DateString(newYear)).toEqual("2024-01-01");
const newYearEve = new Date(2023, 11, 31, 23, 59, 59);
expect(DateFormat.toIso8601DateString(newYearEve)).toEqual("2023-12-31");
});
it("should handle month boundaries", () => {
const endOfMonth = new Date(2024, 9, 31, 23, 59, 59);
expect(DateFormat.toIso8601DateString(endOfMonth)).toEqual("2024-10-31");
const startOfMonth = new Date(2024, 10, 1, 0, 0, 0);
expect(DateFormat.toIso8601DateString(startOfMonth)).toEqual(
"2024-11-01",
);
});
it("should handle very old dates", () => {
const oldDate = new Date(1900, 0, 1);
expect(DateFormat.toIso8601DateString(oldDate)).toEqual("1900-01-01");
});
it("should handle future dates", () => {
const futureDate = new Date(2050, 11, 31);
expect(DateFormat.toIso8601DateString(futureDate)).toEqual("2050-12-31");
});
});
});

View File

@@ -0,0 +1,25 @@
import type { Dayjs } from "dayjs";
import dayjs from "./dayjs";
const TZ = "Europe/Tallinn";
const ISO_8601_DATE = "YYYY-MM-DD";
const ISO_8601_DATE_ZERO_HOURS = `${ISO_8601_DATE}T00:00:00`;
const ISO_8601_DATE_ZERO_MINUTES = `${ISO_8601_DATE}THH:00:00`;
const DateFormat = {
toDayjs(date?: Date | string, format?: string): Dayjs {
return dayjs(date, format).tz(TZ);
},
toIso8601DateString(date: Date | string, format = ISO_8601_DATE): string {
return this.toDayjs(date).format(format);
},
toIso8601DateWithZeroHours(date: Date): string {
return this.toDayjs(date).format(ISO_8601_DATE_ZERO_HOURS);
},
toIso8601DateWithZeroMinutes(date: Date): string {
return this.toDayjs(date).format(ISO_8601_DATE_ZERO_MINUTES);
},
};
export default DateFormat;

View File

@@ -0,0 +1,122 @@
import DateUtil from "./DateUtil";
describe("DateUtil", () => {
it("isDateInRangeIgnoringYear", () => {
expect(
DateUtil.isDateInRangeIgnoringYear(
new Date(2020, 5, 30),
new Date(2020, 0, 31),
new Date(2020, 11, 31),
),
).toBeTruthy();
expect(
DateUtil.isDateInRangeIgnoringYear(
new Date(2020, 1, 32),
new Date(2020, 10, 31),
new Date(2020, 6, 30),
),
).toBeTruthy();
expect(
DateUtil.isDateInRangeIgnoringYear(
new Date(2020, 10, 1),
new Date(2020, 10, 1),
new Date(2020, 6, 30),
),
).toBeTruthy();
expect(
DateUtil.isDateInRangeIgnoringYear(
new Date(2020, 10, 30),
new Date(2020, 10, 31),
new Date(2020, 6, 30),
),
).toBeFalsy();
expect(
DateUtil.isDateInRangeIgnoringYear(
new Date(2020, 6, 16),
new Date(2020, 10, 31),
new Date(2020, 6, 15),
),
).toBeFalsy();
expect(
DateUtil.isDateInRangeIgnoringYear(
new Date(2023, 6, 14),
new Date(2020, 10, 31),
new Date(2020, 6, 15),
),
).toBeTruthy();
});
it("getDateWithSubtractedDays", () => {
expect(
DateUtil.getDateWithSubtractedDays(
new Date("2024-01-15T10:30:00.000Z"),
3,
).toISOString(),
).toEqual("2024-01-12T10:30:00.000Z");
expect(
DateUtil.getDateWithSubtractedDays(
new Date("2024-01-15T10:30:00.000Z"),
0,
).toISOString(),
).toEqual("2024-01-15T10:30:00.000Z");
});
it("getDaysBetweenDates", () => {
expect(
DateUtil.getDaysBetweenDates(
new Date("2024/01/01"),
new Date("2024/01/01"),
),
).toEqual(0);
expect(
DateUtil.getDaysBetweenDates(
new Date("2024/01/01"),
new Date("2024/01/02"),
),
).toEqual(1);
expect(
DateUtil.getDaysBetweenDates(
new Date("2024/01/01"),
new Date("2024/02/01"),
),
).toEqual(31);
expect(
DateUtil.getDaysBetweenDates(
new Date("2024/01/01"),
new Date("2024/12/31"),
),
).toEqual(365);
expect(
DateUtil.getDaysBetweenDates(
new Date("2024/01/01"),
new Date("2025/01/01"),
),
).toEqual(366); // 2024 is a leap year
expect(
DateUtil.getDaysBetweenDates(
new Date("2025/01/01"),
new Date("2026/01/01"),
),
).toEqual(365); // 2025 is not a leap year
expect(
DateUtil.getDaysBetweenDates(
new Date("2024/06/15"),
new Date("2024/12/25"),
),
).toEqual(193);
});
it("getDate", () => {
expect(DateUtil.getDate("2024-10-10")?.toISOString()).toEqual(
"2024-10-10T00:00:00.000Z",
);
expect(DateUtil.getDate(null)).toEqual(null);
expect(
DateUtil.getDate(new Date(2024, 9, 11, 0, 0, 0))?.toISOString(),
).toEqual("2024-10-10T21:00:00.000Z");
});
});

162
web/util/src/DateUtil.ts Normal file
View File

@@ -0,0 +1,162 @@
import NumberUtil from "./NumberUtil";
const DAYS_IN_A_MONTH = 28;
const DateUtil = {
addDaysToDate(date: Date, days: number): Date {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
},
addHoursToDate(date: Date, hours: number): Date {
const result = new Date(date);
result.setHours(result.getHours() + hours);
return result;
},
addMinutesToDate(date: Date, minutes: number): Date {
const result = new Date(date);
result.setMinutes(result.getMinutes() + minutes);
return result;
},
addMonthsToDate(date: Date | string, months: number, isUTCDate?: boolean) {
let currentDate = new Date(date);
const currentDay = DateUtil.getUTCDate(currentDate, isUTCDate);
currentDate.setMonth(currentDate.getMonth() + months, currentDay);
if (
DateUtil.isMonthChangeUTCTimeDifference(
currentDate,
currentDay,
isUTCDate,
) ||
DateUtil.hasInvalidNoOfDaysInMonth(currentDate, currentDay)
) {
currentDate.setDate(0);
}
const decimal = NumberUtil.decimalPart(months);
const daysToAdd = Math.floor(decimal * DAYS_IN_A_MONTH);
if (daysToAdd > 0) {
currentDate = this.addDaysToDate(currentDate, daysToAdd);
}
return currentDate;
},
areDatesEqual(date1: Date, date2: Date): boolean {
return (
date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate()
);
},
areDatesSameHour(date1: Date, date2: Date): boolean {
return (
this.areDatesEqual(date1, date2) && date1.getHours() === date2.getHours()
);
},
getDate(value?: Date | string | null): Date | null {
if (value instanceof Date) {
return value;
}
if (typeof value !== "string") {
return null;
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return null;
}
return date;
},
getDateWithSubtractedDays(date: Date, daysToSubtract: number): Date {
const newDate = new Date(date);
newDate.setDate(newDate.getDate() - daysToSubtract);
return newDate;
},
getDaysBetweenDates(initialDate_: Date, futureDate_: Date): number {
const initialDate = this.zerodate(initialDate_);
const futureDate = this.zerodate(futureDate_);
const timeDiff = futureDate.getTime() - initialDate.getTime();
const daysDiff = Math.floor(timeDiff / (1_000 * 3_600 * 24));
return daysDiff;
},
getFirstDayOfYear: (year: number) => new Date(year, 0, 1),
getLastDayOfYear: (year: number) => new Date(year, 11, 31),
getMonthsBetweenDates(initialDate: Date, futureDate: Date): number {
let months = (futureDate.getFullYear() - initialDate.getFullYear()) * 12;
months -= initialDate.getMonth();
months += futureDate.getMonth();
return months <= 0 ? -1 : months;
},
getMonthsUntilDate(futureDate: Date): number {
const today = new Date();
return this.getMonthsBetweenDates(today, futureDate);
},
getUTCDate: (date: Date, isUTCDate = false) => {
return isUTCDate ? date.getUTCDate() : date.getDate();
},
hasInvalidNoOfDaysInMonth: (calculatedDate: Date, currentDay: number) => {
return calculatedDate.getDate() < currentDay;
},
isDateInRangeIgnoringYear(
date: Date,
startDate: Date,
endDate: Date,
): boolean {
const currentDate = this.zerodate(date);
const startDateIgnoringYear = this.zerodate(startDate);
startDateIgnoringYear.setFullYear(currentDate.getFullYear());
const endDateIgnoringYear = this.zerodate(endDate);
endDateIgnoringYear.setFullYear(currentDate.getFullYear());
if (endDateIgnoringYear < startDateIgnoringYear) {
endDateIgnoringYear.setFullYear(endDateIgnoringYear.getFullYear() + 1);
if (currentDate < startDateIgnoringYear) {
currentDate.setFullYear(currentDate.getFullYear() + 1);
}
}
const isUsageFromInPast = startDateIgnoringYear <= currentDate;
const isUsageToInFuture = endDateIgnoringYear >= currentDate;
const isInRange = isUsageFromInPast && isUsageToInFuture;
return isInRange;
},
isInFuture(date?: Date | null): boolean {
const parsedDate = DateUtil.getDate(date);
return parsedDate !== null && parsedDate > new Date();
},
isInPast(date?: Date | null): boolean {
const parsedDate = DateUtil.getDate(date);
return parsedDate !== null && parsedDate < new Date();
},
isMonthChangeUTCTimeDifference: (
currentDate: Date,
currentDay: number,
isUTCDate?: boolean,
) => {
return (
DateUtil.getUTCDate(currentDate, isUTCDate) < currentDay &&
currentDate.getUTCMonth() < currentDate.getMonth()
);
},
zerodate<T extends Date | null>(date: T): T {
if (date === null) {
return null as T;
}
const value = new Date(date);
value.setHours(0, 0, 0, 0);
return value as T;
},
zerominutes<T extends Date | null>(date: T): T {
if (date === null) {
return null as T;
}
const value = new Date(date);
value.setMinutes(0, 0, 0);
return value as T;
},
};
export default DateUtil;

View File

@@ -0,0 +1,127 @@
/** biome-ignore-all lint/suspicious/noApproximativeNumericConstant: <explanation>great */
import NumberUtil from "./NumberUtil";
describe("NumberUtil", () => {
describe("decimalPart", () => {
test("returns decimal part of positive number", () => {
expect(NumberUtil.decimalPart(3.14159)).toBe(0.14);
});
test("returns decimal part of negative number", () => {
expect(NumberUtil.decimalPart(-3.14159)).toBe(-0.14);
});
test("returns 0 for whole number", () => {
expect(NumberUtil.decimalPart(5)).toBe(0);
});
test("returns 0 for zero", () => {
expect(NumberUtil.decimalPart(0)).toBe(0);
});
test("handles very small decimal parts", () => {
expect(NumberUtil.decimalPart(1.001)).toBe(0);
});
test("handles decimal parts with multiple digits", () => {
expect(NumberUtil.decimalPart(2.999)).toBe(1);
});
});
describe("getValidNumber", () => {
test("returns number when valid number is passed", () => {
expect(NumberUtil.getValidNumber(42)).toBe(42);
});
test("returns number when valid string number is passed", () => {
expect(NumberUtil.getValidNumber("42")).toBe(42);
});
test("returns number when valid float string is passed", () => {
expect(NumberUtil.getValidNumber("3.14")).toBe(3.14);
});
test("returns null when null is passed", () => {
expect(NumberUtil.getValidNumber(null)).toBe(null);
});
test("returns null when undefined is passed", () => {
expect(NumberUtil.getValidNumber(undefined)).toBe(null);
});
test("returns null when empty string is passed", () => {
expect(NumberUtil.getValidNumber("")).toBe(null);
});
test("returns null when invalid string is passed", () => {
expect(NumberUtil.getValidNumber("abc")).toBe(null);
});
test("returns null when NaN is passed", () => {
expect(NumberUtil.getValidNumber(Number.NaN)).toBe(null);
});
test("returns 0 when zero string is passed", () => {
expect(NumberUtil.getValidNumber("0")).toBe(0);
});
test("handles negative numbers", () => {
expect(NumberUtil.getValidNumber(-42)).toBe(-42);
expect(NumberUtil.getValidNumber("-42")).toBe(-42);
});
test("handles floating point numbers", () => {
expect(NumberUtil.getValidNumber(3.14159)).toBe(3.14159);
expect(NumberUtil.getValidNumber("-3.14159")).toBe(-3.14159);
});
});
describe("getValidNumberOrThrow", () => {
test("returns number when valid number is passed", () => {
expect(NumberUtil.getValidNumberOrThrow(42)).toBe(42);
});
test("returns number when valid string number is passed", () => {
expect(NumberUtil.getValidNumberOrThrow("42")).toBe(42);
});
test("throws error when null is passed", () => {
expect(() => NumberUtil.getValidNumberOrThrow(null)).toThrow(
"Invalid number='null'",
);
});
test("throws error when undefined is passed", () => {
expect(() => NumberUtil.getValidNumberOrThrow(undefined)).toThrow(
"Invalid number='undefined'",
);
});
test("throws error when empty string is passed", () => {
expect(() => NumberUtil.getValidNumberOrThrow("")).toThrow(
"Invalid number=''",
);
});
test("throws error when invalid string is passed", () => {
expect(() => NumberUtil.getValidNumberOrThrow("abc")).toThrow(
"Invalid number='abc'",
);
});
test("throws error when NaN is passed", () => {
expect(() => NumberUtil.getValidNumberOrThrow(Number.NaN)).toThrow(
"Invalid number='NaN'",
);
});
test("returns 0 when zero string is passed", () => {
expect(NumberUtil.getValidNumberOrThrow("0")).toBe(0);
});
test("handles negative numbers", () => {
expect(NumberUtil.getValidNumberOrThrow(-42)).toBe(-42);
expect(NumberUtil.getValidNumberOrThrow("-42")).toBe(-42);
});
});
});

View File

@@ -0,0 +1,27 @@
export default class NumberUtil {
public static decimalPart(number_: number): number {
return Number((number_ % 1).toFixed(2));
}
public static getValidNumber(number?: number | string | null): number | null {
if (number === null) {
return null;
}
if (typeof number === "string" && number === "") {
return null;
}
const value = Number(number);
if (Number.isNaN(value)) {
return null;
}
return value;
}
public static getValidNumberOrThrow(number?: number | string | null): number {
const valid = this.getValidNumber(number);
if (typeof valid !== "number" || Number.isNaN(valid)) {
throw new Error(`Invalid number='${number}'`);
}
return valid;
}
}

View File

@@ -0,0 +1,215 @@
import SortUtil from "./SortUtil";
interface TestObject {
active?: boolean;
createdAt?: Date;
id?: number;
name?: string;
value?: number;
}
interface TestObjectWithString {
createdAt: string;
}
interface TestObjectWithDate {
createdAt: Date;
}
describe("SortUtil", () => {
describe("compareByBoolean", () => {
test("returns 0 when both values are true", () => {
const obj1 = { active: true };
const obj2 = { active: true };
expect(SortUtil.compareByBoolean(obj1, obj2, "active")).toBe(0);
});
test("returns 0 when both values are false", () => {
const obj1 = { active: false };
const obj2 = { active: false };
expect(SortUtil.compareByBoolean(obj1, obj2, "active")).toBe(0);
});
test("returns -1 when first value is true and second is false", () => {
const obj1 = { active: true };
const obj2 = { active: false };
expect(SortUtil.compareByBoolean(obj1, obj2, "active")).toBe(-1);
});
test("returns 1 when first value is false and second is true", () => {
const obj1 = { active: false };
const obj2 = { active: true };
expect(SortUtil.compareByBoolean(obj1, obj2, "active")).toBe(1);
});
});
describe("compareByNumber", () => {
test("returns 0 when both objects are undefined", () => {
expect(
SortUtil.compareByNumber<TestObject>(undefined, undefined, "value"),
).toBe(0);
});
test("returns 0 when first object is undefined", () => {
const obj2: TestObject = { value: 5 };
expect(
SortUtil.compareByNumber<TestObject>(undefined, obj2, "value"),
).toBe(0);
});
test("returns 0 when second object is undefined", () => {
const obj1: TestObject = { value: 5 };
expect(
SortUtil.compareByNumber<TestObject>(obj1, undefined, "value"),
).toBe(0);
});
test("returns 0 when values are not numbers", () => {
const obj1 = { value: "not a number" };
const obj2 = { value: "also not a number" };
expect(SortUtil.compareByNumber(obj1, obj2, "value")).toBe(0);
});
test("returns 1 when first value is greater than second", () => {
const obj1: TestObject = { value: 10 };
const obj2: TestObject = { value: 5 };
expect(SortUtil.compareByNumber(obj1, obj2, "value")).toBe(1);
});
test("returns -1 when first value is less than second", () => {
const obj1: TestObject = { value: 5 };
const obj2: TestObject = { value: 10 };
expect(SortUtil.compareByNumber(obj1, obj2, "value")).toBe(-1);
});
test("returns -1 when values are equal", () => {
const obj1: TestObject = { value: 5 };
const obj2: TestObject = { value: 5 };
expect(SortUtil.compareByNumber(obj1, obj2, "value")).toBe(-1);
});
});
describe("compareByText", () => {
test("returns 0 when strings are equal", () => {
const obj1 = { name: "test" };
const obj2 = { name: "test" };
expect(SortUtil.compareByText(obj1, obj2, "name")).toBe(0);
});
test("returns negative when first string comes before second alphabetically", () => {
const obj1 = { name: "apple" };
const obj2 = { name: "banana" };
expect(SortUtil.compareByText(obj1, obj2, "name")).toBeLessThan(0);
});
test("returns positive when first string comes after second alphabetically", () => {
const obj1 = { name: "zebra" };
const obj2 = { name: "apple" };
expect(SortUtil.compareByText(obj1, obj2, "name")).toBeGreaterThan(0);
});
test("handles case insensitive comparison", () => {
const obj1 = { name: "Apple" };
const obj2 = { name: "apple" };
expect(SortUtil.compareByText(obj1, obj2, "name")).toBe(0);
});
test("handles numbers as strings", () => {
const obj1 = { id: 1 };
const obj2 = { id: 2 };
expect(SortUtil.compareByText(obj1, obj2, "id")).toBeLessThan(0);
});
});
describe("sortNumeric", () => {
test("sorts numbers in ascending order", () => {
const numbers = [3, 1, 4, 1, 5];
const result = SortUtil.sortNumeric(numbers);
expect(result).toEqual([1, 1, 3, 4, 5]);
});
test("returns empty array when input is empty", () => {
const result = SortUtil.sortNumeric([]);
expect(result).toEqual([]);
});
test("handles single number", () => {
const result = SortUtil.sortNumeric([42]);
expect(result).toEqual([42]);
});
test("handles negative numbers", () => {
const numbers = [-3, -1, -4, -1, -5];
const result = SortUtil.sortNumeric(numbers);
expect(result).toEqual([-5, -4, -3, -1, -1]);
});
test("handles mixed positive and negative numbers", () => {
const numbers = [3, -1, 4, -2, 0];
const result = SortUtil.sortNumeric(numbers);
expect(result).toEqual([-2, -1, 0, 3, 4]);
});
test("returns 0 for null or undefined values", () => {
// This tests the fallback behavior in the sort function
const numbers = [null as any, undefined as any, 1, 2];
const result = SortUtil.sortNumeric(numbers);
expect(result).toEqual([null, undefined, 1, 2]);
});
test("does not mutate original array", () => {
const numbers = [3, 1, 4];
const original = [...numbers];
SortUtil.sortNumeric(numbers);
expect(numbers).toEqual(original);
});
});
describe("compareByDate", () => {
test("returns 0 when dates are equal", () => {
const date = new Date("2023-01-01");
const obj1: TestObjectWithDate = { createdAt: date };
const obj2: TestObjectWithDate = { createdAt: date };
expect(SortUtil.compareByDate(obj1, obj2, "createdAt")).toBe(0);
});
test("returns negative when first date is before second", () => {
const obj1: TestObjectWithDate = { createdAt: new Date("2023-01-01") };
const obj2: TestObjectWithDate = { createdAt: new Date("2023-01-02") };
expect(SortUtil.compareByDate(obj1, obj2, "createdAt")).toBeLessThan(0);
});
test("returns positive when first date is after second", () => {
const obj1: TestObjectWithDate = { createdAt: new Date("2023-01-02") };
const obj2: TestObjectWithDate = { createdAt: new Date("2023-01-01") };
expect(SortUtil.compareByDate(obj1, obj2, "createdAt")).toBeGreaterThan(
0,
);
});
test("handles date strings", () => {
const obj1: TestObjectWithString = { createdAt: "2023-01-01" };
const obj2: TestObjectWithString = { createdAt: "2023-01-02" };
expect(SortUtil.compareByDate(obj1, obj2, "createdAt")).toBeLessThan(0);
});
test("returns -1 when first date is invalid", () => {
const obj1: TestObjectWithString = { createdAt: "invalid date" };
const obj2: TestObjectWithString = { createdAt: "2023-01-01" };
expect(SortUtil.compareByDate(obj1, obj2, "createdAt")).toBe(-1);
});
test("returns 1 when second date is invalid", () => {
const obj1: TestObjectWithString = { createdAt: "2023-01-01" };
const obj2: TestObjectWithString = { createdAt: "invalid date" };
expect(SortUtil.compareByDate(obj1, obj2, "createdAt")).toBe(1);
});
test("handles both dates being invalid", () => {
const obj1: TestObjectWithString = { createdAt: "invalid date 1" };
const obj2: TestObjectWithString = { createdAt: "invalid date 2" };
// When both are invalid, first gets -1, so result should be negative
expect(SortUtil.compareByDate(obj1, obj2, "createdAt")).toBe(-1);
});
});
});

64
web/util/src/SortUtil.ts Normal file
View File

@@ -0,0 +1,64 @@
import NumberUtil from "./NumberUtil";
export default class SortUtil {
public static compareByBoolean<T>(
object1: T,
object2: T,
key: keyof T,
): number {
// eslint-disable-next-line @typescript-eslint/naming-convention
const x = object1[key] as boolean;
// eslint-disable-next-line @typescript-eslint/naming-convention
const y = object2[key] as boolean;
if (x === y) {
return 0;
}
if (x) {
return -1;
}
return 1;
}
public static compareByNumber<T>(
object1: NonNullable<T> | undefined,
object2: NonNullable<T> | undefined,
key: keyof T,
): number {
const value1 = object1?.[key] as number;
const value2 = object2?.[key] as number;
if (typeof value2 !== "number" || typeof value1 !== "number") {
return 0;
}
return value1 > value2 ? 1 : -1;
}
public static compareByText<T>(object1: T, object2: T, key: keyof T): number {
const value1 = (object1[key] as number | string).toString().toLowerCase();
const value2 = (object2[key] as number | string).toString().toLowerCase();
return value1.localeCompare(value2, "et-EE");
}
public static sortNumeric(list: number[]): number[] {
return [...list].sort((a, b) => {
if (typeof a !== "number" || typeof b !== "number") {
return 0;
}
return a > b ? 1 : -1;
});
}
public static compareByDate<T>(object1: T, object2: T, key: keyof T): number {
const value1 = object1[key] as Date | string;
const value2 = object2[key] as Date | string;
const date1 = value1 instanceof Date ? value1 : new Date(value1);
const date2 = value2 instanceof Date ? value2 : new Date(value2);
if (NumberUtil.getValidNumber(date1.getTime()) === null) {
return -1;
}
if (NumberUtil.getValidNumber(date2.getTime()) === null) {
return 1;
}
return date1.getTime() - date2.getTime();
}
}

View File

@@ -0,0 +1,60 @@
import TextFormat from "./TextFormat";
describe("TextFormat", () => {
it.each([
["Peeter", "Tamm", "Peeter Tamm"],
[null, "Tamm", "Tamm"],
[undefined, "Tamm", "Tamm"],
["", "Tamm", "Tamm"],
["Peeter", undefined, "Peeter"],
["Peeter", null, "Peeter"],
["Peeter", "", "Peeter"],
])(
"toFullName firstName='%s' lastName='%s' -> '%s'",
(firstName, lastName, result) => {
expect(TextFormat.toFullName({ firstName, lastName })).toEqual(result);
},
);
describe("toTitleCase", () => {
test("converts first letter to uppercase", () => {
expect(TextFormat.toTitleCase("hello")).toBe("Hello");
});
test("converts each word to title case", () => {
expect(TextFormat.toTitleCase("hello world")).toBe("Hello World");
});
test("handles empty string", () => {
expect(TextFormat.toTitleCase("")).toBe("");
});
test("handles undefined", () => {
expect(TextFormat.toTitleCase(undefined)).toBe("");
});
test("handles already capitalized text", () => {
expect(TextFormat.toTitleCase("Hello World")).toBe("Hello World");
});
test("handles mixed case text", () => {
expect(TextFormat.toTitleCase("hELLo WoRLd")).toBe("Hello World");
});
test("handles text with apostrophes", () => {
expect(TextFormat.toTitleCase("don't stop")).toBe("Don't Stop");
});
test("handles text with hyphens", () => {
expect(TextFormat.toTitleCase("well-known fact")).toBe("Well-Known Fact");
});
test("handles single character", () => {
expect(TextFormat.toTitleCase("a")).toBe("A");
});
test("handles numbers and special characters", () => {
expect(TextFormat.toTitleCase("test123 @#$")).toBe("Test123 @#$");
});
});
});

View File

@@ -0,0 +1,28 @@
import TextUtil from "./TextUtil";
export default class TextFormat {
public static toFullName({
firstName,
lastName,
}: {
firstName?: string | null;
lastName?: string | null;
}): string {
return [
TextUtil.isEmpty(firstName) ? null : firstName,
TextUtil.isEmpty(lastName) ? null : lastName,
]
.filter(Boolean)
.join(" ");
}
public static toTitleCase(value?: string) {
return (
value
?.toLowerCase()
.replace(/[^-'\s]+/g, (match) =>
match.replace(/^./, (first) => first.toUpperCase()),
) ?? ""
);
}
}

View File

@@ -0,0 +1,113 @@
import TextUtil from "./TextUtil";
describe("TextUtil", () => {
it.each(["", " ", " "])("isEmpty=true for value='%s'", (value) => {
expect(TextUtil.isEmpty(value)).toEqual(true);
});
it.each([undefined, null, [], {}, 5])(
"isEmpty=true for non-string value='%s'",
(value) => {
expect(TextUtil.isEmpty(value)).toEqual(true);
},
);
it.each(["a", " Ab ", "Text"])("isEmpty=false for value='%s'", (value) => {
expect(TextUtil.isEmpty(value)).toEqual(false);
});
it("sanitize", () => {
expect(
TextUtil.sanitizeUnescapedQuotesAndLoadJsonStr<object>(`{
"answer": "A",
"question": "Mida tähendab sõna "pankrot"?",
"answers": {
"A": "Maksujõuetus",
"B": "Pangatöötaja",
"C": "Pankurite pidustus",
"D": "Rikas rott"
},
"reason": "The correct answer is A: Maksujõuetus because this is the proper legal and economic term for bankruptcy, describing a situation where an entity is unable to pay its debts. This is the formal definition used in business and legal contexts."
}`),
).toEqual({
answer: "A",
answers: {
A: "Maksujõuetus",
B: "Pangatöötaja",
C: "Pankurite pidustus",
D: "Rikas rott",
},
question: 'Mida tähendab sõna "pankrot"?',
reason:
"The correct answer is A: Maksujõuetus because this is the proper legal and economic term for bankruptcy, describing a situation where an entity is unable to pay its debts. This is the formal definition used in business and legal contexts.",
});
});
describe("trim", () => {
test("removes all whitespace from string", () => {
expect(TextUtil.trim("hello world")).toBe("helloworld");
});
test("removes tabs and newlines", () => {
expect(TextUtil.trim("hello\tworld\n")).toBe("helloworld");
});
test("returns undefined for undefined input", () => {
expect(TextUtil.trim(undefined)).toBeUndefined();
});
test("returns undefined for null input", () => {
expect(TextUtil.trim(null)).toBeUndefined();
});
test("returns empty string for empty string input", () => {
expect(TextUtil.trim("")).toBe("");
});
test("removes all types of whitespace", () => {
expect(TextUtil.trim(" \t\n\r\f\v ")).toBe("");
});
});
describe("sanitizeUnescapedQuotesAndLoadJsonStr - edge cases", () => {
test("handles valid JSON without unescaped quotes", () => {
const validJson = '{"name": "test", "value": 123}';
const result = TextUtil.sanitizeUnescapedQuotesAndLoadJsonStr<{
name: string;
value: number;
}>(validJson);
expect(result).toEqual({ name: "test", value: 123 });
});
test("throws error for completely invalid JSON", () => {
const invalidJson = "not json at all";
expect(() => {
TextUtil.sanitizeUnescapedQuotesAndLoadJsonStr(invalidJson);
}).toThrow();
});
test("handles multiple unescaped quotes", () => {
const jsonWithMultipleQuotes = '{"text": "He said "hello" and "goodbye""}';
const result = TextUtil.sanitizeUnescapedQuotesAndLoadJsonStr<{
text: string;
}>(jsonWithMultipleQuotes);
expect(result.text).toBe('He said "hello" and "goodbye"');
});
test("handles empty object", () => {
const emptyJson = "{}";
const result = TextUtil.sanitizeUnescapedQuotesAndLoadJsonStr<object>(
emptyJson,
);
expect(result).toEqual({});
});
test("handles array JSON", () => {
const arrayJson = '["item1", "item2"]';
const result = TextUtil.sanitizeUnescapedQuotesAndLoadJsonStr<string[]>(
arrayJson,
);
expect(result).toEqual(["item1", "item2"]);
});
});
});

56
web/util/src/TextUtil.ts Normal file
View File

@@ -0,0 +1,56 @@
export default class TextUtil {
public static isEmpty(string?: unknown | null): boolean {
return typeof string !== "string" || string.trim().length === 0;
}
public static trim(value?: string | null): string | undefined {
return value?.replace(/\s/g, "");
}
public static sanitizeUnescapedQuotesAndLoadJsonStr<T>(s: string): T {
let jsStr = s;
let prevPos = -1;
let currPos = 0;
while (currPos > prevPos) {
// after while check, move marker before we overwrite it
prevPos = currPos;
try {
return JSON.parse(jsStr) as T;
} catch (err) {
// TypeScript doesn't have a direct equivalent to Python's JSONDecodeError
// So we need to extract the position from the error message
if (err instanceof SyntaxError) {
// Extract position from error message
const match = /at position (\d+)/.exec(err.message);
if (match) {
currPos = Number.parseInt(match[1]!, 10);
if (currPos <= prevPos) {
// previous change didn't make progress, so error
throw err;
}
// find the previous " before currPos
const prevQuoteIndex = jsStr.lastIndexOf('"', currPos);
if (prevQuoteIndex !== -1) {
// escape it to \"
jsStr = `${jsStr.slice(0, prevQuoteIndex)}\\${jsStr.slice(
prevQuoteIndex,
)}`;
}
} else {
throw err; // Can't find position in error message
}
} else {
throw err; // Not a JSON syntax error
}
}
}
throw new Error(
"Failed to parse JSON after attempting to fix unescaped quotes",
);
}
}

15
web/util/src/dayjs.ts Normal file
View File

@@ -0,0 +1,15 @@
import dayjs from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
import timezone from "dayjs/plugin/timezone";
import updateLocale from "dayjs/plugin/updateLocale";
import utc from "dayjs/plugin/utc";
const TZ_SHORT = "et/EE";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(updateLocale);
dayjs.updateLocale(TZ_SHORT, { weekStart: 0 });
dayjs.extend(customParseFormat);
export default dayjs;

View File

@@ -0,0 +1,33 @@
const DateFormat = {
date: "dd.MM.yyyy",
dateTime: "dd.MM.yyyy HH:mm",
dateTimeWithSeparator: "dd.MM.yyyy · HH:mm",
shortDate: "dd.MM",
};
const Formatter = {
number: (value?: number, roundTo = 1, precision = 1): string =>
Number.isNaN(Number(value))
? "-"
: // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
Intl.NumberFormat("et-EE", {
maximumFractionDigits: precision,
minimumFractionDigits: precision,
style: "decimal",
}).format(Number(value?.toFixed(roundTo) || 0)),
percentage: (value?: number, roundTo = 1, precision = 1): string =>
`${Formatter.number(value, roundTo, precision)} %`,
temperature: (value?: number): string => `${Formatter.number(value)} °C`,
withDateFormat(value: Date): string {
try {
return value.toISOString();
} catch (e) {
// eslint-disable-next-line no-console
console.warn("Failed to format date", value, e);
return "-";
}
},
};
export { DateFormat, Formatter };
export default Formatter;

View File

@@ -0,0 +1,7 @@
const getAverage = (values: number[]) => {
const sum = values.reduce((a, b) => a + b, 0);
const avg = sum / values.length || 0;
return avg;
};
export default getAverage;

View File

@@ -0,0 +1,27 @@
import secondsUtil from "./secondsUtil";
const getFormattedRuntimeValue = (runtimeDate: Date | null): string => {
if (!runtimeDate || Number.isNaN(new Date(runtimeDate).getTime()) || new Date(runtimeDate).getTime() <= 0) {
return "-";
}
const secondsUntilValue = secondsUtil(runtimeDate);
if (typeof secondsUntilValue !== "number") {
return "-";
}
if (secondsUntilValue < 0) {
const hours = Math.abs(Math.floor(secondsUntilValue / 60 / 60));
if (hours > 6) {
return `-${hours} hr`;
}
//return `-${Math.abs(Math.floor(secondsUntilValue / 60))}`;
return `-${Math.abs(Math.floor(secondsUntilValue / 60))} min`;
}
if (secondsUntilValue < 120) {
return `${secondsUntilValue} sec`;
}
//return `${Math.abs(Math.floor(secondsUntilValue / 60))}`;
return `${Math.abs(Math.floor(secondsUntilValue / 60))} min`;
};
export default getFormattedRuntimeValue;

18
web/util/src/index.ts Normal file
View File

@@ -0,0 +1,18 @@
export { default as ArrayUtil } from './ArrayUtil';
export { default as BooleanUtil } from './BooleanUtil';
export { default as DateUtil } from './DateUtil';
export { default as Formatter } from './formatting';
export { default as getAverage } from './getAverage';
export { default as getFormattedRuntimeValue } from './getFormattedRuntimeValue';
export { default as isDateOlderThanMaxAge } from './isDateOlderThanMaxAge';
export * from './noop';
export { default as NumberUtil } from './NumberUtil';
export { default as resolveNestedKey } from './resolveNestedKey';
export { default as secondsUtil } from './secondsUtil';
export { default as SortUtil } from './SortUtil';
export { default as TextFormat } from './TextFormat';
export { default as TextUtil } from './TextUtil';
export * from './jsonPrettyPrint';
export { default as CommonUtil } from './CommonUtil';
export { default as BlobUtil } from './BlobUtil';
export { default as DateFormat } from './DateFormat';

View File

@@ -0,0 +1,16 @@
function isDateOlderThanMaxAge(
dateString?: string | Date,
maxAgeDays = 0,
): boolean {
if (dateString === undefined) {
return true;
}
const date = new Date(dateString);
const maxAgeMilliseconds = maxAgeDays * 24 * 60 * 60 * 1000;
const currentDate = new Date();
const difference = currentDate.getTime() - date.getTime();
return difference > maxAgeMilliseconds;
}
export default isDateOlderThanMaxAge;

View File

@@ -0,0 +1,22 @@
import { jsonPrettyPrint } from "./jsonPrettyPrint";
const json1 = { a: "a" };
const formatted1 = `{
"a": "a"
}`;
const json2 = {};
const formatted2 = "{}";
describe("jsonPrettyPrint", () => {
it("should work", () => {
expect(jsonPrettyPrint(json1)).toEqual(formatted1);
expect(jsonPrettyPrint(json2)).toEqual(formatted2);
expect(jsonPrettyPrint(1)).toEqual("1");
expect(jsonPrettyPrint("text")).toEqual('"text"');
expect(jsonPrettyPrint("")).toEqual('""');
expect(jsonPrettyPrint(null)).toEqual("null");
expect(jsonPrettyPrint()).toEqual(undefined);
});
});

View File

@@ -0,0 +1,3 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const jsonPrettyPrint = (object?: any): string =>
JSON.stringify(object, undefined, 2);

2
web/util/src/noop.ts Normal file
View File

@@ -0,0 +1,2 @@
export const noop = () => {};
export const noopAsync = async () => {};

View File

@@ -0,0 +1,11 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
const resolveNestedKey = <T = any>(
object: any,
path: string,
defaultValue: any,
): T => path.split(".").reduce((o, p) => (o ? o[p] : defaultValue), object);
export default resolveNestedKey;

View File

@@ -0,0 +1,20 @@
function secondsUtil(isoDate: string | Date): number | null {
try {
const targetDate = new Date(isoDate);
const currentDate = new Date();
const differenceInMilliseconds =
targetDate.getTime() - currentDate.getTime();
if (Number.isNaN(differenceInMilliseconds)) {
console.error("Invalid date provided");
return null;
}
return Math.round(differenceInMilliseconds / 1000);
} catch (error) {
console.error("An error occurred:", error);
return null;
}
}
export default secondsUtil;

11
web/util/tsconfig.json Normal file
View File

@@ -0,0 +1,11 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"composite": true,
"noEmit": false
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "src/**/*.test.ts"]
}