updates
This commit is contained in:
15
web/util/package.json
Normal file
15
web/util/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
43
web/util/src/ArrayUtil.test.ts
Normal file
43
web/util/src/ArrayUtil.test.ts
Normal 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
21
web/util/src/ArrayUtil.ts
Normal 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()))];
|
||||
}
|
||||
}
|
||||
42
web/util/src/BlobUtil.test.ts
Normal file
42
web/util/src/BlobUtil.test.ts
Normal 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
6
web/util/src/BlobUtil.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
21
web/util/src/BooleanUtil.test.ts
Normal file
21
web/util/src/BooleanUtil.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
9
web/util/src/BooleanUtil.ts
Normal file
9
web/util/src/BooleanUtil.ts
Normal 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";
|
||||
}
|
||||
}
|
||||
179
web/util/src/CommonUtil.test.ts
Normal file
179
web/util/src/CommonUtil.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
58
web/util/src/CommonUtil.ts
Normal file
58
web/util/src/CommonUtil.ts
Normal 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;
|
||||
191
web/util/src/DateFormat.test.ts
Normal file
191
web/util/src/DateFormat.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
25
web/util/src/DateFormat.ts
Normal file
25
web/util/src/DateFormat.ts
Normal 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;
|
||||
122
web/util/src/DateUtil.test.ts
Normal file
122
web/util/src/DateUtil.test.ts
Normal 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
162
web/util/src/DateUtil.ts
Normal 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;
|
||||
127
web/util/src/NumberUtil.test.ts
Normal file
127
web/util/src/NumberUtil.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
27
web/util/src/NumberUtil.ts
Normal file
27
web/util/src/NumberUtil.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
215
web/util/src/SortUtil.test.ts
Normal file
215
web/util/src/SortUtil.test.ts
Normal 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
64
web/util/src/SortUtil.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
60
web/util/src/TextFormat.test.ts
Normal file
60
web/util/src/TextFormat.test.ts
Normal 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 @#$");
|
||||
});
|
||||
});
|
||||
});
|
||||
28
web/util/src/TextFormat.ts
Normal file
28
web/util/src/TextFormat.ts
Normal 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()),
|
||||
) ?? ""
|
||||
);
|
||||
}
|
||||
}
|
||||
113
web/util/src/TextUtil.test.ts
Normal file
113
web/util/src/TextUtil.test.ts
Normal 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
56
web/util/src/TextUtil.ts
Normal 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
15
web/util/src/dayjs.ts
Normal 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;
|
||||
33
web/util/src/formatting.ts
Normal file
33
web/util/src/formatting.ts
Normal 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;
|
||||
7
web/util/src/getAverage.ts
Normal file
7
web/util/src/getAverage.ts
Normal 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;
|
||||
27
web/util/src/getFormattedRuntimeValue.ts
Normal file
27
web/util/src/getFormattedRuntimeValue.ts
Normal 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
18
web/util/src/index.ts
Normal 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';
|
||||
16
web/util/src/isDateOlderThanMaxAge.ts
Normal file
16
web/util/src/isDateOlderThanMaxAge.ts
Normal 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;
|
||||
22
web/util/src/jsonPrettyPrint.test.ts
Normal file
22
web/util/src/jsonPrettyPrint.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
3
web/util/src/jsonPrettyPrint.ts
Normal file
3
web/util/src/jsonPrettyPrint.ts
Normal 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
2
web/util/src/noop.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const noop = () => {};
|
||||
export const noopAsync = async () => {};
|
||||
11
web/util/src/resolveNestedKey.ts
Normal file
11
web/util/src/resolveNestedKey.ts
Normal 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;
|
||||
20
web/util/src/secondsUtil.ts
Normal file
20
web/util/src/secondsUtil.ts
Normal 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
11
web/util/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user