Files
test/backend/src/utils/misc.ts
Benjamin Falch 2bc741fb78
Some checks failed
Mark Stale PRs / stale (push) Has been cancelled
adding monkeytype
2026-04-23 13:53:44 +02:00

260 lines
6.8 KiB
TypeScript

import { MILLISECONDS_IN_DAY } from "@monkeytype/util/date-and-time";
import { roundTo2 } from "@monkeytype/util/numbers";
import uaparser from "ua-parser-js";
import { MonkeyRequest } from "../api/types";
import { ObjectId } from "mongodb";
//todo split this file into smaller util files (grouped by functionality)
export function identity(value: unknown): string {
return Object.prototype.toString
.call(value)
.replace(/^\[object\s+([a-z]+)\]$/i, "$1")
.toLowerCase();
}
export function base64UrlEncode(data: string): string {
return Buffer.from(data).toString("base64url");
}
export function base64UrlDecode(data: string): string {
return Buffer.from(data, "base64url").toString();
}
type AgentLog = {
ip: string;
agent: string;
device?: string;
};
export function buildAgentLog(req: MonkeyRequest): AgentLog {
const agent = uaparser(req.raw.headers["user-agent"]);
const agentLog: AgentLog = {
ip:
(req.raw.headers["cf-connecting-ip"] as string) ||
(req.raw.headers["x-forwarded-for"] as string) ||
(req.raw.ip as string) ||
"255.255.255.255",
agent: `${agent.os.name} ${agent.os.version} ${agent.browser.name} ${agent.browser.version}`,
};
const {
device: { vendor, model, type },
} = agent;
agentLog.device = `${vendor ?? "unknown vendor"} ${
model ?? "unknown model"
} ${type ?? "unknown type"}`;
return agentLog;
}
export function padNumbers(
numbers: number[],
maxLength: number,
fillString: string,
): string[] {
return numbers.map((number) =>
number.toString().padStart(maxLength, fillString),
);
}
export function matchesAPattern(text: string, pattern: string): boolean {
const regex = new RegExp(`^${pattern}$`);
return regex.test(text);
}
export function kogascore(wpm: number, acc: number, timestamp: number): number {
// its safe to round after multiplying by 100 (99.99 * 100 rounded will be 9999 not 100)
// rounding is necessary to protect against floating point errors
const normalizedWpm = Math.round(wpm * 100);
const normalizedAcc = Math.round(acc * 100);
const padAmount = 100000;
const firstPart = (padAmount + normalizedWpm) * padAmount;
const secondPart = (firstPart + normalizedAcc) * padAmount;
const currentDayTimeMilliseconds =
timestamp - (timestamp % MILLISECONDS_IN_DAY);
const todayMilliseconds = timestamp - currentDayTimeMilliseconds;
return (
secondPart + Math.floor((MILLISECONDS_IN_DAY - todayMilliseconds) / 1000)
);
}
export function flattenObjectDeep(
obj: Record<string, unknown>,
prefix = "",
): Record<string, unknown> {
const result: Record<string, unknown> = {};
const keys = Object.keys(obj);
keys.forEach((key) => {
const value = obj[key];
const newPrefix = prefix.length > 0 ? `${prefix}.${key}` : key;
if (isPlainObject(value)) {
const flattened = flattenObjectDeep(value as Record<string, unknown>);
const flattenedKeys = Object.keys(flattened);
if (flattenedKeys.length === 0) {
result[newPrefix] = value;
}
flattenedKeys.forEach((flattenedKey) => {
result[`${newPrefix}.${flattenedKey}`] = flattened[flattenedKey];
});
} else {
result[newPrefix] = value;
}
});
return result;
}
export function sanitizeString(str: string | undefined): string | undefined {
if (str === undefined || str === "") {
return str;
}
return str
.replace(/[\u0300-\u036F]/g, "")
.trim()
.replace(/\n{3,}/g, "\n\n")
.replace(/\s{3,}/g, " ");
}
const suffixes = ["th", "st", "nd", "rd"];
export function getOrdinalNumberString(number: number): string {
const lastTwo = number % 100;
const suffix =
suffixes[(lastTwo - 20) % 10] ?? suffixes[lastTwo] ?? suffixes[0];
return `${number}${suffix}`;
}
type TimeUnit =
| "second"
| "minute"
| "hour"
| "day"
| "week"
| "month"
| "year";
export const MINUTE_IN_SECONDS = 1 * 60;
export const HOUR_IN_SECONDS = 1 * 60 * MINUTE_IN_SECONDS;
export const DAY_IN_SECONDS = 1 * 24 * HOUR_IN_SECONDS;
export const WEEK_IN_SECONDS = 1 * 7 * DAY_IN_SECONDS;
export const MONTH_IN_SECONDS = 1 * 30.4167 * DAY_IN_SECONDS;
export const YEAR_IN_SECONDS = 1 * 12 * MONTH_IN_SECONDS;
export function formatSeconds(
seconds: number,
): `${number} ${TimeUnit}${"s" | ""}` {
let unit: TimeUnit;
let secondsInUnit: number;
if (seconds < MINUTE_IN_SECONDS) {
unit = "second";
secondsInUnit = 1;
} else if (seconds < HOUR_IN_SECONDS) {
unit = "minute";
secondsInUnit = MINUTE_IN_SECONDS;
} else if (seconds < DAY_IN_SECONDS) {
unit = "hour";
secondsInUnit = HOUR_IN_SECONDS;
} else if (seconds < WEEK_IN_SECONDS) {
unit = "day";
secondsInUnit = DAY_IN_SECONDS;
} else if (seconds < YEAR_IN_SECONDS) {
if (seconds < WEEK_IN_SECONDS * 4) {
unit = "week";
secondsInUnit = WEEK_IN_SECONDS;
} else {
unit = "month";
secondsInUnit = MONTH_IN_SECONDS;
}
} else {
unit = "year";
secondsInUnit = YEAR_IN_SECONDS;
}
const normalized = roundTo2(seconds / secondsInUnit);
return `${normalized} ${unit}${normalized > 1 ? "s" : ""}`;
}
export function isDevEnvironment(): boolean {
return process.env["MODE"] === "dev";
}
export function getFrontendUrl(): string {
return isDevEnvironment()
? "http://localhost:3000"
: (process.env["FRONTEND_URL"] ?? "https://monkeytype.com");
}
/**
* convert database object into api object
* @param data database object with `_id: ObjectId`
* @returns api object with `id: string`
*/
export function replaceObjectId<T extends { _id: ObjectId }>(
data: T,
): T & { _id: string };
export function replaceObjectId<T extends { _id: ObjectId }>(
data: T | null,
): (T & { _id: string }) | null;
export function replaceObjectId<T extends { _id: ObjectId }>(
data: T | null,
): (T & { _id: string }) | null {
if (data === null) {
return null;
}
const result = {
...data,
_id: data._id.toString(),
} as T & { _id: string };
return result;
}
/**
* convert database objects into api objects
* @param data database objects with `_id: ObjectId`
* @returns api objects with `id: string`
*/
export function replaceObjectIds<T extends { _id: ObjectId }>(
data: T[],
): (T & { _id: string })[] {
if (data === undefined) return data;
return data.map((it) => replaceObjectId(it));
}
export type WithObjectId<T extends { _id: string }> = Omit<T, "_id"> & {
_id: ObjectId;
};
export function omit<T extends object, K extends keyof T>(
obj: T,
keys: K[],
): Omit<T, K> {
const result = { ...obj };
for (const key of keys) {
// oxlint-disable-next-line no-dynamic-delete
delete result[key];
}
return result;
}
export function isPlainObject(value: unknown): boolean {
return (
value !== null &&
typeof value === "object" &&
Object.getPrototypeOf(value) === Object.prototype
);
}