UNPKG

@dpkit/table

Version:

Data Package implementation in TypeScript.

153 lines (130 loc) 4.42 kB
import type { Field, Schema } from "@dpkit/core" import { col } from "nodejs-polars" import { getPolarsSchema } from "../schema/index.ts" import type { Table } from "../table/index.ts" // TODO: Review default values being {fields: []} vs undefined export type InferSchemaOptions = { sampleRows?: number confidence?: number commaDecimal?: boolean monthFirst?: boolean } export async function inferSchema(table: Table, options?: InferSchemaOptions) { const { sampleRows = 100, confidence = 0.9 } = options ?? {} const schema: Schema = { fields: [], } const typeMapping = createTypeMapping() const regexMapping = createRegexMapping(options) const sample = await table.head(sampleRows).collect() const polarsSchema = getPolarsSchema(sample.schema) const failureThreshold = sample.height - Math.floor(sample.height * confidence) || 1 for (const polarsField of polarsSchema.fields) { const name = polarsField.name const type = typeMapping[polarsField.type.variant] ?? "any" let field = { name, type } if (type === "string") { for (const [regex, patch] of Object.entries(regexMapping)) { const failures = sample .filter(col(name).str.contains(regex).not()) .head(failureThreshold).height if (failures < failureThreshold) { field = { ...field, ...patch } break } } } schema.fields.push(field) } return schema } function createTypeMapping() { const mapping: Record<string, Field["type"]> = { Array: "array", Bool: "boolean", Categorical: "string", Date: "date", Datetime: "datetime", Decimal: "number", Float32: "number", Float64: "number", Int16: "integer", Int32: "integer", Int64: "integer", Int8: "integer", List: "array", Null: "any", Object: "object", String: "string", Struct: "object", Time: "time", UInt16: "integer", UInt32: "integer", UInt64: "integer", UInt8: "integer", Utf8: "string", } return mapping } function createRegexMapping(options?: { commaDecimal?: boolean monthFirst?: boolean }) { const { commaDecimal, monthFirst } = options ?? {} const mapping: Record<string, Partial<Field>> = { // Numeric "^\\d+$": { type: "integer" }, "^[,\\d]+$": commaDecimal ? { type: "number" } : { type: "integer", groupChar: "," }, "^\\d+\\.\\d+$": commaDecimal ? { type: "integer", groupChar: "." } : { type: "number" }, "^[,\\d]+\\.\\d+$": { type: "number", groupChar: "," }, "^[.\\d]+\\,\\d+$": { type: "number", groupChar: ".", decimalChar: "," }, // Boolean "^(true|True|TRUE|false|False|FALSE)$": { type: "boolean" }, // Date "^\\d{4}-\\d{2}-\\d{2}$": { type: "date" }, "^\\d{4}/\\d{2}/\\d{2}$": { type: "date", format: "%Y/%m/%d" }, "^\\d{2}/\\d{2}/\\d{4}$": monthFirst ? { type: "date", format: "%m/%d/%Y" } : { type: "date", format: "%d/%m/%Y" }, "^\\d{2}-\\d{2}-\\d{4}$": monthFirst ? { type: "date", format: "%m-%d-%Y" } : { type: "date", format: "%d-%m-%Y" }, "^\\d{2}\\.\\d{2}\\.\\d{4}$": monthFirst ? { type: "date", format: "%m.%d.%Y" } : { type: "date", format: "%d.%m.%Y" }, // Time "^\\d{2}:\\d{2}:\\d{2}$": { type: "time" }, "^\\d{2}:\\d{2}$": { type: "time", format: "%H:%M" }, "^\\d{1,2}:\\d{2}:\\d{2}\\s*(am|pm|AM|PM)$": { type: "time", format: "%I:%M:%S %p", }, "^\\d{1,2}:\\d{2}\\s*(am|pm|AM|PM)$": { type: "time", format: "%I:%M %p" }, "^\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:?\\d{2}$": { type: "time" }, // Datetime - ISO format "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z?$": { type: "datetime" }, "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:?\\d{2}$": { type: "datetime", }, "^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$": { type: "datetime", format: "%Y-%m-%d %H:%M:%S", }, "^\\d{2}/\\d{2}/\\d{4} \\d{2}:\\d{2}$": monthFirst ? { type: "datetime", format: "%m/%d/%Y %H:%M" } : { type: "datetime", format: "%d/%m/%Y %H:%M" }, "^\\d{2}/\\d{2}/\\d{4} \\d{2}:\\d{2}:\\d{2}$": monthFirst ? { type: "datetime", format: "%m/%d/%Y %H:%M:%S" } : { type: "datetime", format: "%d/%m/%Y %H:%M:%S" }, // Object "^\\{": { type: "object" }, // Array "^\\[": { type: "array" }, } return mapping }