import * as uuid from "uuid"; import { z } from "zod"; import { get_empty_datum_for, type Presentation, } from "./presentation.svelte.ts"; type Assert<_T extends true> = void; export const all_datum_tags = [ "Numeric", "Text", "Timestamp", "Uuid", ] as const; // Type checking to ensure that all valid enum tags are included. type _DatumTagsAssertionA = Assert< Datum["t"] extends (typeof all_datum_tags)[number] ? true : false >; 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), }); const datum_text_schema = z.object({ t: z.literal("Text"), c: z.string().nullish().transform((x) => x ?? undefined), }); const datum_timestamp_schema = z.object({ t: z.literal("Timestamp"), c: z.coerce.date().nullish().transform((x) => x ?? undefined), }); const datum_uuid_schema = z.object({ t: z.literal("Uuid"), c: z.string().nullish().transform((x) => x ?? undefined), }); export const datum_schema = z.union([ datum_numeric_schema, datum_text_schema, datum_timestamp_schema, datum_uuid_schema, ]); export type Datum = z.infer; /** * 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( 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); } } 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, }; } 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") { try { return { t: "Uuid", c: uuid.stringify(uuid.parse(input)), }; } catch { // uuid.parse() throws a TypeError if unsuccessful. return get_empty_datum_for(presentation); } } type _ = Assert; throw new Error("should be unreachable"); }