2025-11-11 01:26:48 +00:00
|
|
|
import * as uuid from "uuid";
|
2025-09-08 15:56:57 -07:00
|
|
|
import { z } from "zod";
|
2025-11-11 01:26:48 +00:00
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
get_empty_datum_for,
|
|
|
|
|
type Presentation,
|
|
|
|
|
} from "./presentation.svelte.ts";
|
2025-09-08 15:56:57 -07:00
|
|
|
|
|
|
|
|
type Assert<_T extends true> = void;
|
|
|
|
|
|
2025-09-23 13:08:51 -07:00
|
|
|
export const all_datum_tags = [
|
2025-11-11 01:26:48 +00:00
|
|
|
"Numeric",
|
2025-09-08 15:56:57 -07:00
|
|
|
"Text",
|
|
|
|
|
"Timestamp",
|
|
|
|
|
"Uuid",
|
|
|
|
|
] as const;
|
|
|
|
|
|
|
|
|
|
// Type checking to ensure that all valid enum tags are included.
|
2025-11-11 01:26:48 +00:00
|
|
|
type _DatumTagsAssertionA = Assert<
|
2025-09-23 13:08:51 -07:00
|
|
|
Datum["t"] extends (typeof all_datum_tags)[number] ? true : false
|
2025-09-08 15:56:57 -07:00
|
|
|
>;
|
2025-11-11 01:26:48 +00:00
|
|
|
type _DatumTagsAssertionB = Assert<
|
|
|
|
|
(typeof all_datum_tags)[number] extends Datum["t"] ? true : false
|
|
|
|
|
>;
|
|
|
|
|
|
|
|
|
|
const NUMERIC_REGEX = /^-?[0-9]+(\.[0-9]+)?$/;
|
|
|
|
|
|
|
|
|
|
const datum_numeric_schema = z.object({
|
|
|
|
|
t: z.literal("Numeric"),
|
|
|
|
|
// The `bigdecimal` Rust library serializes values to strings by default.
|
|
|
|
|
// Doing the same here helps to avoid weird floating point conversion errors
|
|
|
|
|
// when displaying.
|
|
|
|
|
c: z.string().regex(NUMERIC_REGEX).nullish().transform((x) => x ?? undefined),
|
|
|
|
|
});
|
2025-09-08 15:56:57 -07:00
|
|
|
|
2025-09-23 13:08:51 -07:00
|
|
|
const datum_text_schema = z.object({
|
2025-09-08 15:56:57 -07:00
|
|
|
t: z.literal("Text"),
|
|
|
|
|
c: z.string().nullish().transform((x) => x ?? undefined),
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-23 13:08:51 -07:00
|
|
|
const datum_timestamp_schema = z.object({
|
2025-09-08 15:56:57 -07:00
|
|
|
t: z.literal("Timestamp"),
|
|
|
|
|
c: z.coerce.date().nullish().transform((x) => x ?? undefined),
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-23 13:08:51 -07:00
|
|
|
const datum_uuid_schema = z.object({
|
2025-09-08 15:56:57 -07:00
|
|
|
t: z.literal("Uuid"),
|
|
|
|
|
c: z.string().nullish().transform((x) => x ?? undefined),
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-23 13:08:51 -07:00
|
|
|
export const datum_schema = z.union([
|
2025-11-11 01:26:48 +00:00
|
|
|
datum_numeric_schema,
|
2025-09-23 13:08:51 -07:00
|
|
|
datum_text_schema,
|
|
|
|
|
datum_timestamp_schema,
|
|
|
|
|
datum_uuid_schema,
|
2025-09-08 15:56:57 -07:00
|
|
|
]);
|
|
|
|
|
|
2025-09-23 13:08:51 -07:00
|
|
|
export type Datum = z.infer<typeof datum_schema>;
|
2025-11-10 08:43:33 +00:00
|
|
|
|
2025-11-11 01:26:48 +00:00
|
|
|
/**
|
|
|
|
|
* Attempt to parse a Datum from a textual representation, for example when
|
|
|
|
|
* pasting values from the clipboard. If unsuccessful, returns an empty Datum.
|
|
|
|
|
*/
|
|
|
|
|
export function parse_datum_from_text(
|
2025-11-10 08:43:33 +00:00
|
|
|
presentation: Presentation,
|
|
|
|
|
input: string,
|
|
|
|
|
): Datum {
|
|
|
|
|
if (presentation.t === "Dropdown") {
|
|
|
|
|
if (
|
|
|
|
|
presentation.c.allow_custom ||
|
|
|
|
|
presentation.c.options.some(({ value }) => value === input)
|
|
|
|
|
) {
|
|
|
|
|
return { t: "Text", c: input };
|
|
|
|
|
} else {
|
|
|
|
|
return get_empty_datum_for(presentation);
|
|
|
|
|
}
|
2025-11-11 01:26:48 +00:00
|
|
|
} else if (presentation.t === "Numeric") {
|
|
|
|
|
// For now, commas and spaces are accepted as thousands-separators, but full
|
|
|
|
|
// stops are not. Only full stops are allowed as the decimal separator. This
|
|
|
|
|
// is about as flexible as parsing can be with regards to international
|
|
|
|
|
// standards from an English-centric perspective, without resorting to
|
|
|
|
|
// troublesome heuristics to disambiguate between thousands-separators and
|
|
|
|
|
// decimal separators.
|
|
|
|
|
const input_cleaned = input.trim().replace(/^\+/, "").replace(
|
|
|
|
|
/(?:,|\s+)([0-9]{3})(,|.|\s|$)/g,
|
|
|
|
|
"$1$2",
|
|
|
|
|
);
|
|
|
|
|
console.log(NUMERIC_REGEX.test(input_cleaned));
|
|
|
|
|
return {
|
|
|
|
|
t: "Numeric",
|
|
|
|
|
c: NUMERIC_REGEX.test(input_cleaned) ? input_cleaned : undefined,
|
|
|
|
|
};
|
2025-11-10 08:43:33 +00:00
|
|
|
} else if (presentation.t === "Text") {
|
|
|
|
|
return { t: "Text", c: input };
|
|
|
|
|
} else if (presentation.t === "Timestamp") {
|
|
|
|
|
// TODO: implement
|
|
|
|
|
console.warn("parsing timestamps from clipboard is not yet supported");
|
|
|
|
|
return get_empty_datum_for(presentation);
|
|
|
|
|
} else if (presentation.t === "Uuid") {
|
2025-11-11 01:26:48 +00:00
|
|
|
try {
|
|
|
|
|
return {
|
|
|
|
|
t: "Uuid",
|
|
|
|
|
c: uuid.stringify(uuid.parse(input)),
|
|
|
|
|
};
|
|
|
|
|
} catch {
|
|
|
|
|
// uuid.parse() throws a TypeError if unsuccessful.
|
|
|
|
|
return get_empty_datum_for(presentation);
|
|
|
|
|
}
|
2025-11-10 08:43:33 +00:00
|
|
|
}
|
|
|
|
|
type _ = Assert<typeof presentation["t"] extends never ? true : false>;
|
|
|
|
|
throw new Error("should be unreachable");
|
|
|
|
|
}
|