adding monkeytype
Some checks failed
Mark Stale PRs / stale (push) Has been cancelled

This commit is contained in:
Benjamin Falch
2026-04-23 13:53:44 +02:00
parent e214a2fd35
commit 2bc741fb78
1930 changed files with 7590652 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
{
"ignorePatterns": ["node_modules", "dist", ".turbo"],
"extends": [
"../oxlint-config/index.jsonc"
// "@monkeytype/oxlint-config"
]
}

View File

@@ -0,0 +1,49 @@
import { describe, it, expect } from "vitest";
import * as Arrays from "../src/arrays";
describe("arrays", () => {
it("intersect", () => {
const testCases = [
{
a: [1],
b: [2],
removeDuplicates: false,
expected: [],
},
{
a: [1],
b: [1],
removeDuplicates: false,
expected: [1],
},
{
a: [1, 1],
b: [1],
removeDuplicates: true,
expected: [1],
},
{
a: [1, 1],
b: [1],
removeDuplicates: false,
expected: [1, 1],
},
{
a: [1],
b: [1, 2, 3],
removeDuplicates: false,
expected: [1],
},
{
a: [1, 1],
b: [1, 2, 3],
removeDuplicates: true,
expected: [1],
},
];
testCases.forEach(({ a, b, removeDuplicates, expected }) => {
expect(Arrays.intersect(a, b, removeDuplicates)).toEqual(expected);
});
});
});

View File

@@ -0,0 +1,223 @@
import { describe, it, expect, afterAll, vi } from "vitest";
import * as DateAndTime from "../src/date-and-time";
describe("date-and-time", () => {
afterAll(() => {
vi.useRealTimers();
});
it("getCurrentDayTimestamp", () => {
vi.useFakeTimers();
vi.setSystemTime(1652743381);
const currentDay = DateAndTime.getCurrentDayTimestamp();
expect(currentDay).toBe(1641600000);
});
it("getStartOfWeekTimestamp", () => {
const testCases = [
{
input: 1662400184017, // Mon Sep 05 2022 17:49:44 GMT+0000
expected: 1662336000000, // Mon Sep 05 2022 00:00:00 GMT+0000
},
{
input: 1559771456000, // Wed Jun 05 2019 21:50:56 GMT+0000
expected: 1559520000000, // Mon Jun 03 2019 00:00:00 GMT+0000
},
{
input: 1465163456000, // Sun Jun 05 2016 21:50:56 GMT+0000
expected: 1464566400000, // Mon May 30 2016 00:00:00 GMT+0000
},
{
input: 1491515456000, // Thu Apr 06 2017 21:50:56 GMT+0000
expected: 1491177600000, // Mon Apr 03 2017 00:00:00 GMT+0000
},
{
input: 1462507200000, // Fri May 06 2016 04:00:00 GMT+0000
expected: 1462147200000, // Mon May 02 2016 00:00:00 GMT+0000
},
{
input: 1231218000000, // Tue Jan 06 2009 05:00:00 GMT+0000,
expected: 1231113600000, // Mon Jan 05 2009 00:00:00 GMT+0000
},
{
input: 1709420681000, // Sat Mar 02 2024 23:04:41 GMT+0000
expected: 1708905600000, // Mon Feb 26 2024 00:00:00 GMT+0000
},
];
testCases.forEach(({ input, expected }) => {
expect(DateAndTime.getStartOfWeekTimestamp(input)).toEqual(expected);
});
});
it("getCurrentWeekTimestamp", () => {
Date.now = vi.fn(() => 825289481000); // Sun Feb 25 1996 23:04:41 GMT+0000
const currentWeek = DateAndTime.getCurrentWeekTimestamp();
expect(currentWeek).toBe(824688000000); // Mon Feb 19 1996 00:00:00 GMT+0000
});
it("getStartOfDayTimestamp", () => {
const testCases = [
{
input: new Date("2023/06/16 15:00 UTC").getTime(),
offset: 0,
expected: new Date("2023/06/16 00:00 UTC").getTime(), // Mon Sep 05 2022 00:00:00 GMT+0000
},
{
input: new Date("2023/06/16 15:00 UTC").getTime(),
offset: 1,
expected: new Date("2023/06/16 01:00 UTC").getTime(), // Mon Sep 05 2022 00:00:00 GMT+0000
},
{
input: new Date("2023/06/16 15:00 UTC").getTime(),
offset: -1,
expected: new Date("2023/06/15 23:00 UTC").getTime(), // Mon Sep 05 2022 00:00:00 GMT+0000
},
{
input: new Date("2023/06/16 15:00 UTC").getTime(),
offset: -4,
expected: new Date("2023/06/15 20:00 UTC").getTime(), // Mon Sep 05 2022 00:00:00 GMT+0000
},
{
input: new Date("2023/06/16 15:00 UTC").getTime(),
offset: 4,
expected: new Date("2023/06/16 04:00 UTC").getTime(), // Mon Sep 05 2022 00:00:00 GMT+0000
},
{
input: new Date("2023/06/17 03:00 UTC").getTime(),
offset: 4,
expected: new Date("2023/06/16 04:00 UTC").getTime(), // Mon Sep 05 2022 00:00:00 GMT+0000
},
{
input: new Date("2023/06/16 15:00 UTC").getTime(),
offset: 3,
expected: new Date("2023/06/16 03:00 UTC").getTime(), // Mon Sep 05 2022 00:00:00 GMT+0000
},
{
input: new Date("2023/06/17 01:00 UTC").getTime(),
offset: 3,
expected: new Date("2023/06/16 03:00 UTC").getTime(), // Mon Sep 05 2022 00:00:00 GMT+0000
},
];
testCases.forEach(({ input, offset, expected }) => {
expect(
DateAndTime.getStartOfDayTimestamp(input, offset * 3600000),
).toEqual(expected);
});
});
it("isToday", () => {
const testCases = [
{
now: new Date("2023/06/16 15:00 UTC").getTime(),
input: new Date("2023/06/16 15:00 UTC").getTime(),
offset: 0,
expected: true,
},
{
now: new Date("2023/06/16 15:00 UTC").getTime(),
input: new Date("2023/06/17 1:00 UTC").getTime(),
offset: 0,
expected: false,
},
{
now: new Date("2023/06/16 15:00 UTC").getTime(),
input: new Date("2023/06/16 01:00 UTC").getTime(),
offset: 1,
expected: true,
},
{
now: new Date("2023/06/16 15:00 UTC").getTime(),
input: new Date("2023/06/17 01:00 UTC").getTime(),
offset: 2,
expected: true,
},
{
now: new Date("2023/06/16 15:00 UTC").getTime(),
input: new Date("2023/06/16 01:00 UTC").getTime(),
offset: 2,
expected: false,
},
{
now: new Date("2023/06/17 01:00 UTC").getTime(),
input: new Date("2023/06/16 15:00 UTC").getTime(),
offset: 2,
expected: true,
},
{
now: new Date("2023/06/17 01:00 UTC").getTime(),
input: new Date("2023/06/17 02:00 UTC").getTime(),
offset: 2,
expected: false,
},
];
testCases.forEach(({ now, input, offset, expected }) => {
Date.now = vi.fn(() => now);
expect(DateAndTime.isToday(input, offset)).toEqual(expected);
});
});
it("isYesterday", () => {
const testCases = [
{
now: new Date("2023/06/15 15:00 UTC").getTime(),
input: new Date("2023/06/14 15:00 UTC").getTime(),
offset: 0,
expected: true,
},
{
now: new Date("2023/06/15 15:00 UTC").getTime(),
input: new Date("2023/06/15 15:00 UTC").getTime(),
offset: 0,
expected: false,
},
{
now: new Date("2023/06/15 15:00 UTC").getTime(),
input: new Date("2023/06/16 15:00 UTC").getTime(),
offset: 0,
expected: false,
},
{
now: new Date("2023/06/15 15:00 UTC").getTime(),
input: new Date("2023/06/13 15:00 UTC").getTime(),
offset: 0,
expected: false,
},
{
now: new Date("2023/06/16 02:00 UTC").getTime(),
input: new Date("2023/06/15 02:00 UTC").getTime(),
offset: 4,
expected: true,
},
{
now: new Date("2023/06/16 02:00 UTC").getTime(),
input: new Date("2023/06/16 01:00 UTC").getTime(),
offset: 4,
expected: false,
},
{
now: new Date("2023/06/16 02:00 UTC").getTime(),
input: new Date("2023/06/15 22:00 UTC").getTime(),
offset: 4,
expected: false,
},
{
now: new Date("2023/06/16 04:00 UTC").getTime(),
input: new Date("2023/06/16 03:00 UTC").getTime(),
offset: 4,
expected: true,
},
{
now: new Date("2023/06/16 14:00 UTC").getTime(),
input: new Date("2023/06/16 12:00 UTC").getTime(),
offset: -11,
expected: true,
},
];
testCases.forEach(({ now, input, offset, expected }) => {
Date.now = vi.fn(() => now);
expect(DateAndTime.isYesterday(input, offset)).toEqual(expected);
});
});
});

View File

@@ -0,0 +1,147 @@
import { describe, it, expect } from "vitest";
import { parseWithSchema } from "../src/json";
import { z } from "zod";
describe("json", () => {
describe("parseWithSchema", () => {
const schema = z.object({
test: z.boolean().optional(),
name: z.string(),
nested: z.object({ foo: z.string() }).strict().optional(),
});
it("should throw with invalid json", () => {
expect(() => parseWithSchema("blah", schema)).toThrow(
new Error(
`Invalid JSON: Unexpected token 'b', "blah" is not valid JSON`,
),
);
});
it("should parse", () => {
const json = `{
"test":true,
"name":"bob",
"unknown":"unknown",
"nested":{
"foo":"bar"
}
}`;
expect(parseWithSchema(json, schema)).toStrictEqual({
test: true,
name: "bob",
nested: { foo: "bar" },
});
});
it("should throw with invalid schema", () => {
const json = `{
"test":"yes",
"nested":{
"foo":1
}
}`;
expect(() => parseWithSchema(json, schema)).toThrow(
new Error(
`JSON does not match schema: "test" expected boolean, received string, "name" required, "nested.foo" expected string, received number`,
),
);
});
it("should migrate if valid json", () => {
const json = `{
"name": 1
}`;
const result = parseWithSchema(json, schema, {
migrate: () => {
return {
name: "migrated",
test: false,
};
},
});
expect(result).toStrictEqual({
name: "migrated",
test: false,
});
});
it("should revert to fallback if invalid json", () => {
const json = `blah`;
const result = parseWithSchema(json, schema, {
fallback: {
name: "migrated",
test: false,
},
});
expect(result).toStrictEqual({
name: "migrated",
test: false,
});
});
it("should throw if migration fails", () => {
const json = `{
"name": 1
}`;
expect(() => {
parseWithSchema(json, schema, {
//@ts-expect-error need to test migration failure
migrate: () => {
return {
name: null,
test: "Hi",
};
},
});
}).toThrow(
new Error(
`Migrated value does not match schema: "test" expected boolean, received string, "name" expected string, received null`,
),
);
});
it("should revert to fallback if migration fails", () => {
const json = `{
"name": 1
}`;
const result = parseWithSchema(json, schema, {
fallback: {
name: "fallback",
test: false,
},
//@ts-expect-error need to test migration failure
migrate: () => {
return {
name: null,
test: "Hi",
};
},
});
expect(result).toStrictEqual({
name: "fallback",
test: false,
});
});
it("migrate function should receive value", () => {
const json = `{
"test":"test"
}`;
const result = parseWithSchema(json, schema, {
migrate: (value) => {
expect(value).toEqual({ test: "test" });
return {
name: "valid",
};
},
});
expect(result).toStrictEqual({
name: "valid",
});
});
});
});

View File

@@ -0,0 +1,157 @@
import { describe, it, expect } from "vitest";
import * as Numbers from "../src/numbers";
describe("numbers", () => {
describe("roundTo1", () => {
it("should correctly round", () => {
const tests = [
{
in: 0.0,
out: 0,
},
{
in: 0.01,
out: 0.0,
},
{
in: 0.09,
out: 0.1,
},
{
in: 0.123,
out: 0.1,
},
{
in: 0.456,
out: 0.5,
},
{
in: 0.789,
out: 0.8,
},
];
tests.forEach((test) => {
expect(Numbers.roundTo1(test.in)).toBe(test.out);
});
});
it("mapRange", () => {
const testCases = [
{
input: {
value: 123,
inMin: 0,
inMax: 200,
outMin: 0,
outMax: 1000,
clamp: false,
},
expected: 615,
},
{
input: {
value: 123,
inMin: 0,
inMax: 200,
outMin: 1000,
outMax: 0,
clamp: false,
},
expected: 385,
},
{
input: {
value: 10001,
inMin: 0,
inMax: 10000,
outMin: 0,
outMax: 1000,
clamp: false,
},
expected: 1000.1,
},
{
input: {
value: 10001,
inMin: 0,
inMax: 10000,
outMin: 0,
outMax: 1000,
clamp: true,
},
expected: 1000,
},
];
testCases.forEach(({ input, expected }) => {
expect(
Numbers.mapRange(
input.value,
input.inMin,
input.inMax,
input.outMin,
input.outMax,
input.clamp,
),
).toEqual(expected);
});
});
});
describe("isSafeNumber", () => {
describe("should correctly identify safe numbers", () => {
const testCases = [
//safe
{ input: 0, expected: true },
{ input: 1, expected: true },
{ input: -1, expected: true },
{ input: 0.5, expected: true },
{ input: -0.5, expected: true },
//not safe
{ input: NaN, expected: false },
{ input: Infinity, expected: false },
{ input: -Infinity, expected: false },
{ input: "string", expected: false },
{ input: null, expected: false },
{ input: undefined, expected: false },
{ input: true, expected: false },
{ input: false, expected: false },
];
it.for(testCases)(
"should return $expected for $input",
({ input, expected }) => {
expect(Numbers.isSafeNumber(input)).toEqual(expected);
},
);
});
});
describe("safeNumber", () => {
describe("should correctly identify safe numbers", () => {
const testCases = [
//safe
{ input: 0, expected: 0 },
{ input: 1, expected: 1 },
{ input: -1, expected: -1 },
{ input: 0.5, expected: 0.5 },
{ input: -0.5, expected: -0.5 },
//not safe
{ input: NaN, expected: undefined },
{ input: Infinity, expected: undefined },
{ input: -Infinity, expected: undefined },
{ input: "string", expected: undefined },
{ input: null, expected: undefined },
{ input: undefined, expected: undefined },
{ input: true, expected: undefined },
{ input: false, expected: undefined },
];
it.for(testCases)(
"should return $expected for $input",
({ input, expected }) => {
expect(Numbers.safeNumber(input as number)).toEqual(expected);
},
);
});
});
});

View File

@@ -0,0 +1,41 @@
import { describe, it, expect } from "vitest";
import { not } from "../src/predicates";
describe("predicates", () => {
describe("not", () => {
it("should not a simple boolean function", () => {
const isTrue = (): boolean => true;
const isFalse = not(isTrue);
expect(isFalse()).toBe(false);
});
it("should not a numeric predicate", () => {
const isPositive = (num: number): boolean => num > 0;
const isNotPositive = not(isPositive);
expect(isNotPositive(-5)).toBe(true);
expect(isNotPositive(10)).toBe(false);
});
it("should not a predicate taking multiple arguments", () => {
const containsLetter = (
str1: string,
str2: string,
letter: string,
): boolean => str1.includes(letter) || str2.includes(letter);
const doesNotContainLetter = not(containsLetter);
expect(doesNotContainLetter("hello", "world", "x")).toBe(true);
expect(doesNotContainLetter("apple", "banana", "a")).toBe(false);
});
it("should preserve type safety", () => {
const isEven = (num: number): boolean => num % 2 === 0;
const isOdd = not(isEven);
expect(isOdd(3)).toBe(true);
expect(isOdd(4)).toBe(false);
});
});
});

View File

@@ -0,0 +1,14 @@
import { describe, expect, it } from "vitest";
import { kebabToCamelCase } from "../src/strings";
describe("strings", () => {
describe("kebabToCamelCase", () => {
it("should convert kebab case to camel case", () => {
expect(kebabToCamelCase("hello-world")).toEqual("helloWorld");
expect(kebabToCamelCase("helloWorld")).toEqual("helloWorld");
expect(
kebabToCamelCase("one-two-three-four-five-six-seven-eight-nine-ten"),
).toEqual("oneTwoThreeFourFiveSixSevenEightNineTen");
});
});
});

View File

@@ -0,0 +1,92 @@
import { describe, it, expect } from "vitest";
import { tryCatch, tryCatchSync } from "../src/trycatch";
describe("tryCatch", () => {
it("should return data on successful promise resolution", async () => {
const result = await tryCatch(Promise.resolve("success"));
expect(result.data).toBe("success");
expect(result.error).toBeNull();
});
it("should return error on promise rejection", async () => {
const testError = new Error("test error");
const result = await tryCatch(Promise.reject(testError));
expect(result.data).toBeNull();
expect(result.error).toBe(testError);
});
it("should handle custom error types", async () => {
class CustomError extends Error {
code: string;
constructor(message: string, code: string) {
super(message);
this.code = code;
}
}
const customError = new CustomError("custom error", "E123");
const result = await tryCatch<string, CustomError>(
Promise.reject(customError),
);
expect(result.data).toBeNull();
expect(result.error).toBe(customError);
expect(result.error?.code).toBe("E123");
});
it("should handle exceptions in async functions", async () => {
const testError = new Error("test error");
const fn = async (): Promise<void> => {
throw testError;
};
const result = await tryCatch(fn());
expect(result.data).toBeNull();
expect(result.error).toBe(testError);
});
});
describe("tryCatchSync", () => {
it("should return data on successful function execution", () => {
const result = tryCatchSync(() => "success");
expect(result.data).toBe("success");
expect(result.error).toBeNull();
});
it("should return error when function throws", () => {
const testError = new Error("test error");
const result = tryCatchSync(() => {
throw testError;
});
expect(result.data).toBeNull();
expect(result.error).toBe(testError);
});
it("should handle complex data structures", () => {
const complexData = {
foo: "bar",
numbers: [1, 2, 3],
nested: { value: true },
};
const result = tryCatchSync(() => complexData);
expect(result.data).toEqual(complexData);
expect(result.error).toBeNull();
});
it("should handle custom error types", () => {
class CustomError extends Error {
code: string;
constructor(message: string, code: string) {
super(message);
this.code = code;
}
}
const customError = new CustomError("custom error", "E123");
const result = tryCatchSync<string, CustomError>(() => {
throw customError;
});
expect(result.data).toBeNull();
expect(result.error).toBe(customError);
expect(result.error?.code).toBe("E123");
});
});

View File

@@ -0,0 +1,7 @@
{
"extends": "@monkeytype/typescript-config/base.json",
"compilerOptions": {
"noEmit": true
},
"include": ["./**/*.ts", "./**/*.spec.ts", "./setup-tests.ts"]
}

View File

@@ -0,0 +1,30 @@
{
"name": "@monkeytype/util",
"exports": {
"./*": {
"types": "./src/*.ts",
"import": "./dist/*.mjs",
"require": "./dist/*.js"
}
},
"scripts": {
"dev": "tsup-node --watch",
"build": "npm run madge && tsup-node",
"test": "vitest run",
"madge": " madge --circular --extensions ts ./src",
"ts-check": "tsc --noEmit",
"lint": "oxlint . --type-aware --type-check",
"lint-fast": "oxlint ."
},
"devDependencies": {
"@monkeytype/tsup-config": "workspace:*",
"@monkeytype/typescript-config": "workspace:*",
"madge": "8.0.0",
"oxlint": "1.60.0",
"oxlint-tsgolint": "0.21.0",
"tsup": "8.4.0",
"typescript": "6.0.2",
"vitest": "4.1.0",
"zod": "3.23.8"
}
}

View File

@@ -0,0 +1,15 @@
/**
* Returns the intersection of two arrays, i.e., the elements that are present in both arrays.
* @param a First array.
* @param b Second array.
* @returns An array containing the elements that are present in both input arrays.
*/
export function intersect<T>(a: T[], b: T[], removeDuplicates = false): T[] {
if (b.length > a.length) {
[a, b] = [b, a]; // Swap a and b to loop over the shorter array
}
const filtered = a.filter(function (e) {
return b.includes(e);
});
return removeDuplicates ? [...new Set(filtered)] : filtered;
}

View File

@@ -0,0 +1,90 @@
export const MILISECONDS_IN_HOUR = 3600000;
export const MILLISECONDS_IN_DAY = 86400000;
/**
* Returns the timestamp of the start of the day for the given timestamp adjusted by the offset.
* @param timestamp The timestamp for which to get the start of the day.
* @param offsetMilis The offset in milliseconds. Default is 0.
* @returns The timestamp of the start of the day for the given timestamp adjusted by the offset.
*/
export function getStartOfDayTimestamp(
timestamp: number,
offsetMilis = 0,
): number {
return timestamp - ((timestamp - offsetMilis) % MILLISECONDS_IN_DAY);
}
/**
* Returns the current day's start timestamp adjusted by the hour offset.
* @param hourOffset The offset in hours. Default is 0.
* @returns The timestamp of the start of the current day adjusted by the hour offset.
*/
export function getCurrentDayTimestamp(hourOffset = 0): number {
const offsetMilis = hourOffset * MILISECONDS_IN_HOUR;
const currentTime = Date.now();
return getStartOfDayTimestamp(currentTime, offsetMilis);
}
/**
* Checks if the given timestamp is from yesterday, adjusted by the hour offset.
* @param timestamp The timestamp to check.
* @param hourOffset The offset in hours. Default is 0.
* @returns True if the timestamp is from yesterday, false otherwise.
*/
export function isYesterday(timestamp: number, hourOffset = 0): boolean {
const offsetMilis = hourOffset * MILISECONDS_IN_HOUR;
const yesterday = getStartOfDayTimestamp(
Date.now() - MILLISECONDS_IN_DAY,
offsetMilis,
);
const date = getStartOfDayTimestamp(timestamp, offsetMilis);
return yesterday === date;
}
/**
* Checks if the given timestamp is from today, adjusted by the hour offset.
* @param timestamp The timestamp to check.
* @param hourOffset The offset in hours. Default is 0.
* @returns True if the timestamp is from today, false otherwise.
*/
export function isToday(timestamp: number, hourOffset = 0): boolean {
const offsetMilis = hourOffset * MILISECONDS_IN_HOUR;
const today = getStartOfDayTimestamp(Date.now(), offsetMilis);
const date = getStartOfDayTimestamp(timestamp, offsetMilis);
return today === date;
}
/**
* Gets the timestamp of the start of the week for the given timestamp.
* @param timestamp The timestamp for which to get the start of the week.
* @returns The timestamp of the start of the week for the given timestamp.
*/
export function getStartOfWeekTimestamp(timestamp: number): number {
const date = new Date(getStartOfDayTimestamp(timestamp));
const monday = date.getDate() - (date.getDay() || 7) + 1;
date.setDate(monday);
return getStartOfDayTimestamp(date.getTime());
}
/**
* Gets the current week's start timestamp.
* @returns The timestamp of the start of the current week.
*/
export function getCurrentWeekTimestamp(): number {
const currentTime = Date.now();
return getStartOfWeekTimestamp(currentTime);
}
/**
* Gets the timestamp of the start of the last week.
* @returns The timestamp of the start of the last week.
*/
export function getLastWeekTimestamp(): number {
const currentTime = Date.now();
const lastWeekTime = currentTime - 7 * MILLISECONDS_IN_DAY;
return getStartOfWeekTimestamp(lastWeekTime);
}

71
packages/util/src/json.ts Normal file
View File

@@ -0,0 +1,71 @@
import { z, ZodIssue } from "zod";
import { tryCatchSync } from "./trycatch";
function prettyErrorMessage(issue: ZodIssue | undefined): string {
if (issue === undefined) return "";
const path = issue.path.length > 0 ? `"${issue.path.join(".")}" ` : "";
return `${path}${issue.message.toLowerCase()}`;
}
/**
* Parse a JSON string into an object and validate it against a schema
* @param json JSON string
* @param schema Zod schema to validate the JSON against
* @param fallbackAndMigrate Optional object containing optional fallback value and optional migration function
* @throws Error if JSON is invalid and no fallback is provided
* @throws Error if JSON does not match schema and no migration function is provided
* @returns The parsed JSON object
*/
export function parseWithSchema<T extends z.ZodTypeAny>(
json: string,
schema: T,
fallbackAndMigrate?: {
fallback?: z.infer<T>;
migrate?: (
value: Record<string, unknown> | unknown[],
zodIssues?: ZodIssue[],
) => z.infer<T>;
},
): z.infer<T> {
const { fallback, migrate } = fallbackAndMigrate ?? {};
const { data: jsonParsed, error } = tryCatchSync(
() => JSON.parse(json) as Record<string, unknown>,
);
if (error) {
if (fallback === undefined) {
throw new Error(`Invalid JSON: ` + error.message);
}
return fallback as unknown;
}
const safeParse = schema.safeParse(jsonParsed);
if (safeParse.success) {
return safeParse.data as T;
}
if (migrate === undefined) {
throw new Error(
`JSON does not match schema: ${safeParse.error.issues
.map(prettyErrorMessage)
.join(", ")}`,
);
}
const migrated = migrate(jsonParsed, safeParse.error.issues);
const safeParseMigrated = schema.safeParse(migrated);
if (!safeParseMigrated.success) {
if (fallback === undefined) {
throw new Error(
`Migrated value does not match schema: ${safeParseMigrated.error.issues
.map(prettyErrorMessage)
.join(", ")}`,
);
}
return fallback as unknown;
}
return safeParseMigrated.data as T;
}

View File

@@ -0,0 +1,158 @@
//pin implementations
const random = Math.random;
const ceil = Math.ceil;
const floor = Math.floor;
/**
* Rounds a number to one decimal places.
* @param num The number to round.
* @returns The input number rounded to one decimal places.
*/
export function roundTo1(num: number): number {
return Math.round((num + Number.EPSILON) * 10) / 10;
}
/**
* Rounds a number to two decimal places.
* @param num The number to round.
* @returns The input number rounded to two decimal places.
*/
export function roundTo2(num: number): number {
return Math.round((num + Number.EPSILON) * 100) / 100;
}
/**
* Calculates the mean (average) of an array of numbers.
* @param array An array of numbers.
* @returns The mean of the input array.
*/
export function mean(array: number[]): number {
try {
return (
array.reduce((previous, current) => (current += previous)) / array.length
);
} catch (e) {
return 0;
}
}
/**
* Calculates the standard deviation of an array of numbers.
* @param array An array of numbers.
* @returns The standard deviation of the input array.
*/
export function stdDev(array: number[]): number {
try {
const n = array.length;
const meanValue = mean(array);
return Math.sqrt(
array.map((x) => Math.pow(x - meanValue, 2)).reduce((a, b) => a + b) / n,
);
} catch (e) {
return 0;
}
}
/**
* Calculates the median of an array of numbers.
* https://www.w3resource.com/javascript-exercises/fundamental/javascript-fundamental-exercise-88.php
* @param arr An array of numbers.
* @returns The median of the input array.
*/
export function median(arr: number[]): number {
try {
const mid = Math.floor(arr.length / 2),
nums = [...arr].sort((a, b) => a - b);
return arr.length % 2 !== 0
? (nums[mid] as number)
: ((nums[mid - 1] as number) + (nums[mid] as number)) / 2;
} catch (e) {
return 0;
}
}
/**
* Calculates consistency by mapping COV from [0, +infinity) to [100, 0).
* The mapping function is a version of the sigmoid function tanh(x) that is closer to the identity function tanh(arctanh(x)) in [0, 1).
* @param cov The coefficient of variation of an array of numbers (standard deviation / mean).
* @returns Consistency
*/
export function kogasa(cov: number): number {
return (
100 * (1 - Math.tanh(cov + Math.pow(cov, 3) / 3 + Math.pow(cov, 5) / 5))
);
}
/**
* Gets an integer between min and max, both are inclusive.
* @param min
* @param max
* @returns Random integer betwen min and max.
*/
export function randomIntFromRange(min: number, max: number): number {
const minNorm = ceil(min);
const maxNorm = floor(max);
return floor(random() * (maxNorm - minNorm + 1) + minNorm);
}
/**
* Maps a value from one range to another.
* @param value The value to map.
* @param inMin Input range minimum.
* @param inMax Input range maximum.
* @param outMin Output range minimum.
* @param outMax Output range maximum.
* @param clamp If true, the result is clamped to the output range. Default true.
* @returns The mapped value.
*/
export function mapRange(
value: number,
inMin: number,
inMax: number,
outMin: number,
outMax: number,
clamp = true,
): number {
if (inMin === inMax) {
return outMin;
}
const result =
((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
if (clamp) {
if (outMin < outMax) {
return Math.min(Math.max(result, outMin), outMax);
} else {
return Math.max(Math.min(result, outMin), outMax);
}
}
return result;
}
/**
* Checks if a value is a safe number. Safe numbers are finite and not NaN.
* @param value The value to check.
* @returns True if the value is a safe number, false otherwise.
*/
export function isSafeNumber(value: unknown): value is number {
if (typeof value === "number") {
return !isNaN(value) && isFinite(value);
}
return false;
}
/**
* Converts a number to a safe number or undefined. NaN, Infinity, and -Infinity are converted to undefined.
* @param value The value to convert.
* @returns The input number if it is safe, undefined otherwise.
*/
export function safeNumber(
value: number | undefined | null,
): number | undefined {
if (isSafeNumber(value)) {
return value;
}
return undefined;
}

View File

@@ -0,0 +1,12 @@
export type Predicate<Args extends unknown[]> = (...args: Args) => boolean;
/**
* Negates a predicate function, returning a new function that returns the opposite boolean result.
*
* @param predicate - The function to negate.
* @returns A new function that returns the negated boolean result.
*/
export function not<T extends unknown[]>(
predicate: Predicate<T>,
): Predicate<T> {
return (...args: T) => !predicate(...args);
}

View File

@@ -0,0 +1,3 @@
export function kebabToCamelCase(kebab: string): string {
return kebab.replace(/-([a-z])/g, (_, char: string) => char.toUpperCase());
}

View File

@@ -0,0 +1,33 @@
// based on https://gist.github.com/t3dotgg/a486c4ae66d32bf17c09c73609dacc5b
type Success<T> = {
data: T;
error: null;
};
type Failure<E> = {
data: null;
error: E;
};
type Result<T, E = Error> = Success<T> | Failure<E>;
export async function tryCatch<T, E = Error>(
promiseOrFunction: Promise<T>,
): Promise<Result<T, E>> {
try {
let data = await promiseOrFunction;
return { data, error: null };
} catch (error) {
return { data: null, error: error as E };
}
}
export function tryCatchSync<T, E = Error>(fn: () => T): Result<T, E> {
try {
let data = fn();
return { data, error: null };
} catch (error) {
return { data: null, error: error as E };
}
}

12
packages/util/src/zod.ts Normal file
View File

@@ -0,0 +1,12 @@
import { ZodError } from "zod";
//from https://github.com/colinhacks/zod/pull/3819
export function isZodError(error: unknown): error is ZodError {
if (!(error instanceof Error)) return false;
if (error instanceof ZodError) return true;
if (error.constructor.name === "ZodError") return true;
if ("issues" in error && Array.isArray(error.issues)) return true;
return false;
}

View File

@@ -0,0 +1,13 @@
{
"extends": "@monkeytype/typescript-config/base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"moduleResolution": "NodeNext",
"module": "NodeNext",
"target": "ES2015",
"lib": ["es2016"]
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,3 @@
import { extendConfig } from "@monkeytype/tsup-config";
export default extendConfig();

View File

@@ -0,0 +1,10 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "node",
coverage: {
include: ["**/*.ts"],
},
},
});