@nshiab/simple-data-analysis-core 2.1.9 → 2.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,17 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.default = wrapString;
7
+ const splitGraphemes_js_1 = __importDefault(require("./splitGraphemes.js"));
4
8
  /**
5
9
  * Wraps a string to a specified maximum width, attempting to break at word
6
10
  * boundaries when possible. If a single word exceeds the maximum width, it
7
- * will be broken at the character boundary. This function is primarily used
8
- * for preparing text to be displayed in console tables.
11
+ * will be broken at a grapheme boundary, keeping combining accents with their
12
+ * letters. This function is primarily used for preparing text to be displayed
13
+ * in console tables. Widths count graphemes, not terminal columns for wide
14
+ * characters or emoji.
9
15
  *
10
16
  * @example
11
17
  * ```typescript
@@ -29,56 +35,64 @@ exports.default = wrapString;
29
35
  *
30
36
  * @param str - The string to wrap.
31
37
  *
32
- * @param maxWidth - The maximum width of each line.
38
+ * @param maxWidth - The maximum number of graphemes in each line.
33
39
  *
34
40
  * @param wordWrap - If true, attempts to break at word boundaries. If false,
35
- * breaks at character boundaries. Defaults to `true`.
41
+ * breaks at grapheme boundaries. Defaults to `true`.
36
42
  *
37
43
  * @returns The wrapped string with newline characters inserted at appropriate
38
44
  * positions.
39
45
  */
40
46
  function wrapString(str, maxWidth, wordWrap = true) {
41
- if (str.length <= maxWidth) {
47
+ const graphemes = (0, splitGraphemes_js_1.default)(str);
48
+ if (graphemes.length <= maxWidth) {
42
49
  return str;
43
50
  }
44
51
  if (!wordWrap) {
45
- // Simple character-based wrapping
52
+ // Simple grapheme-based wrapping
46
53
  const lines = [];
47
- for (let i = 0; i < str.length; i += maxWidth) {
48
- lines.push(str.slice(i, i + maxWidth));
54
+ for (let i = 0; i < graphemes.length; i += maxWidth) {
55
+ lines.push(graphemes.slice(i, i + maxWidth).join(""));
49
56
  }
50
57
  return lines.join("\n");
51
58
  }
52
59
  // Word-aware wrapping
53
60
  const lines = [];
54
61
  let currentLine = "";
62
+ let currentWidth = 0;
55
63
  const words = str.split(/(\s+)/); // Split but keep whitespace
56
64
  for (const word of words) {
65
+ const wordGraphemes = (0, splitGraphemes_js_1.default)(word);
66
+ const wordWidth = wordGraphemes.length;
57
67
  // If adding this word would exceed maxWidth
58
- if (currentLine.length + word.length > maxWidth) {
68
+ if (currentWidth + wordWidth > maxWidth) {
59
69
  // If current line is not empty, save it
60
70
  if (currentLine.length > 0) {
61
71
  lines.push(currentLine.trimEnd());
62
72
  currentLine = "";
73
+ currentWidth = 0;
63
74
  }
64
75
  // If the word itself is longer than maxWidth, break it
65
- if (word.length > maxWidth) {
66
- for (let i = 0; i < word.length; i += maxWidth) {
67
- const chunk = word.slice(i, i + maxWidth);
68
- if (i + maxWidth < word.length) {
76
+ if (wordWidth > maxWidth) {
77
+ for (let i = 0; i < wordWidth; i += maxWidth) {
78
+ const chunk = wordGraphemes.slice(i, i + maxWidth).join("");
79
+ if (i + maxWidth < wordWidth) {
69
80
  lines.push(chunk);
70
81
  }
71
82
  else {
72
83
  currentLine = chunk;
84
+ currentWidth = wordWidth - i;
73
85
  }
74
86
  }
75
87
  }
76
88
  else {
77
89
  currentLine = word;
90
+ currentWidth = wordWidth;
78
91
  }
79
92
  }
80
93
  else {
81
94
  currentLine += word;
95
+ currentWidth += wordWidth;
82
96
  }
83
97
  }
84
98
  // Add any remaining text
@@ -1,5 +1,5 @@
1
1
  import type SimpleTable from "../class/SimpleTable.js";
2
2
  type Options = NonNullable<Parameters<SimpleTable["umap"]>[1]>;
3
- export default function umap(table: SimpleTable, column: string, options?: Options): void;
3
+ export default function umap(table: SimpleTable, columns: string | string[], options?: Options): void;
4
4
  export {};
5
5
  //# sourceMappingURL=umap.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"umap.d.ts","sourceRoot":"","sources":["../../src/methods/umap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAavD,KAAK,OAAO,GAAG,WAAW,CAAC,UAAU,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE/D,MAAM,CAAC,OAAO,UAAU,IAAI,CAC1B,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,OAAY,QAStB"}
1
+ {"version":3,"file":"umap.d.ts","sourceRoot":"","sources":["../../src/methods/umap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAcvD,KAAK,OAAO,GAAG,WAAW,CAAC,UAAU,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE/D,MAAM,CAAC,OAAO,UAAU,IAAI,CAC1B,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,EAC1B,OAAO,GAAE,OAAY,QAUtB"}
@@ -9,22 +9,24 @@ const quoteIdentifier_js_1 = __importDefault(require("../helpers/quoteIdentifier
9
9
  const queryDB_js_1 = __importDefault(require("../helpers/queryDB.js"));
10
10
  const mergeOptions_js_1 = __importDefault(require("../helpers/mergeOptions.js"));
11
11
  const buildUmapGraph_js_1 = __importDefault(require("../helpers/buildUmapGraph.js"));
12
+ const prepareNumericFeatures_js_1 = __importDefault(require("../helpers/prepareNumericFeatures.js"));
12
13
  const prepareUmapLayoutOptions_js_1 = __importDefault(require("../helpers/prepareUmapLayoutOptions.js"));
13
14
  const readScalarNumber_js_1 = __importDefault(require("../helpers/readScalarNumber.js"));
14
15
  const optimizeUmapLayout_js_1 = __importDefault(require("../helpers/optimizeUmapLayout.js"));
15
16
  const initialUmapCoordinates_js_1 = __importDefault(require("../helpers/initialUmapCoordinates.js"));
16
17
  const appendColumnBatches_js_1 = __importDefault(require("../helpers/appendColumnBatches.js"));
17
18
  const node_api_1 = require("@duckdb/node-api");
18
- function umap(table, column, options = {}) {
19
+ function umap(table, columns, options = {}) {
20
+ const selected = typeof columns === "string" ? columns : [...columns];
19
21
  const settings = structuredClone(options);
20
22
  (0, queueOp_js_1.default)(table, {
21
23
  kind: "barrier",
22
24
  method: "umap()",
23
- parameters: { column, options: settings },
24
- execute: () => execute(table, column, settings),
25
+ parameters: { columns: selected, options: settings },
26
+ execute: () => execute(table, selected, settings),
25
27
  });
26
28
  }
27
- async function execute(table, column, options) {
29
+ async function execute(table, columns, options) {
28
30
  const { neighbors = 15, metric = "euclidean" } = options;
29
31
  if (!Number.isSafeInteger(neighbors) || neighbors < 2) {
30
32
  throw new Error("neighbors must be a safe integer of at least 2.");
@@ -33,22 +35,10 @@ async function execute(table, column, options) {
33
35
  if (!["euclidean", "cosine"].includes(metric)) {
34
36
  throw new Error("metric must be euclidean or cosine.");
35
37
  }
36
- const types = await table.getTypes();
37
- const columns = Object.keys(types);
38
- const resolve = (name) => {
39
- const found = columns.find((c) => c.toLowerCase() === name.toLowerCase());
40
- if (found === undefined)
41
- throw new Error(`Column ${name} does not exist.`);
42
- return found;
43
- };
44
- const vector = resolve(column);
45
- if (columns.some((c) => ["umapx", "umapy"].includes(c.toLowerCase()))) {
38
+ const sourceColumns = await table.getColumns();
39
+ if (sourceColumns.some((c) => ["umapx", "umapy"].includes(c.toLowerCase()))) {
46
40
  throw new Error("UMAP output columns umapX and umapY must not already exist.");
47
41
  }
48
- if (!/^(FLOAT|DOUBLE|TINYINT|SMALLINT|INTEGER|BIGINT|HUGEINT|UTINYINT|USMALLINT|UINTEGER|UBIGINT|UHUGEINT|DECIMAL\(\d+,\d+\))\[\d*\]$/
49
- .test(types[vector])) {
50
- throw new Error("UMAP requires a one-dimensional numeric LIST or ARRAY column.");
51
- }
52
42
  const connection = table.connection;
53
43
  const [sourceOid, sourceTemporary] = (await connection.runAndReadAll(`SELECT table_oid,temporary FROM duckdb_tables()
54
44
  WHERE lower(table_name)=lower($1) AND schema_name=current_schema()
@@ -58,10 +48,18 @@ async function execute(table, column, options) {
58
48
  throw new Error("UMAP requires a materialized source table.");
59
49
  }
60
50
  const q = quoteIdentifier_js_1.default;
51
+ const count = await (0, readScalarNumber_js_1.default)(connection, `SELECT count(*) FROM ${q(table.name)}`);
52
+ if (count < 3 || count > 0x7fffffff) {
53
+ throw new Error("UMAP requires between 3 and 2147483647 rows.");
54
+ }
55
+ const prepared = await (0, prepareNumericFeatures_js_1.default)(table, typeof columns === "string"
56
+ ? { kind: "vector", column: columns }
57
+ : { kind: "scalars", columns }, { method: "umap()" });
58
+ const dimensions = prepared.dimensions;
61
59
  const suffix = crypto.randomUUID().replaceAll("-", "");
62
60
  const name = (part) => `__sda_umap_${suffix}_${part}`;
63
- const ordinal = q(name("ordinal"));
64
- const source = q(table.name), snapshot = q(name("snapshot"));
61
+ const ordinal = q(prepared.rowIdColumn);
62
+ const source = q(table.name), snapshot = q(prepared.relation);
65
63
  const layoutName = name("layout"), layout = q(layoutName);
66
64
  const names = {
67
65
  rows: q(name("rows")),
@@ -74,33 +72,15 @@ async function execute(table, column, options) {
74
72
  graph: q(name("graph")),
75
73
  };
76
74
  const scratch = [
77
- snapshot,
78
75
  layout,
79
76
  ...Object.entries(names)
80
77
  .filter(([key]) => key !== "hnsw").map(([, value]) => value),
81
78
  ];
82
79
  try {
83
- // Retain payloads in native DuckDB types. A private ordinal distinguishes
84
- // duplicate rows and is independent of any user column named rowid.
85
- await connection.run(`CREATE TEMP TABLE ${snapshot} AS
86
- SELECT *, row_number() OVER () - 1 AS ${ordinal} FROM ${source}`);
87
- const count = await (0, readScalarNumber_js_1.default)(connection, `SELECT count(*) FROM ${snapshot}`);
88
- if (count < 3 || count > 0x7fffffff) {
89
- throw new Error("UMAP requires between 3 and 2147483647 rows.");
90
- }
91
- if (await (0, readScalarNumber_js_1.default)(connection, `SELECT count(*) FROM ${snapshot}
92
- WHERE ${q(vector)} IS NULL OR len(${q(vector)})=0
93
- OR list_count(${q(vector)}) != len(${q(vector)})
94
- OR NOT list_bool_and(list_transform(${q(vector)}, x -> isfinite(x::DOUBLE)))`)) {
95
- throw new Error("Vectors must be nonempty, non-null and contain finite numbers only.");
96
- }
97
- if (await (0, readScalarNumber_js_1.default)(connection, `SELECT count(DISTINCT len(${q(vector)})) FROM ${snapshot}`) !== 1) {
98
- throw new Error("Vectors must have equal dimensions.");
99
- }
100
- const dimensions = await (0, readScalarNumber_js_1.default)(connection, `SELECT len(${q(vector)}) FROM ${snapshot} LIMIT 1`);
101
80
  await connection.run(`CREATE TEMP TABLE ${names.rows} AS
102
81
  SELECT (row_number() OVER (ORDER BY ${ordinal})-1)::INTEGER AS vertex,
103
- ${ordinal} AS ordinal, ${q(vector)}::DOUBLE[${dimensions}] AS vec FROM ${snapshot}`);
82
+ ${ordinal} AS ordinal, ${q(prepared.vectorColumn)} AS vec FROM ${snapshot}`);
83
+ await connection.run(`ALTER TABLE ${snapshot} DROP COLUMN ${q(prepared.vectorColumn)}`);
104
84
  if (await (0, readScalarNumber_js_1.default)(connection, `SELECT count(*) FROM ${names.rows}
105
85
  WHERE NOT isfinite(array_inner_product(vec,vec)) OR array_inner_product(vec,vec)>1e300
106
86
  ${metric === "cosine" ? "OR array_inner_product(vec,vec)=0" : ""}`)) {
@@ -137,12 +117,12 @@ async function execute(table, column, options) {
137
117
  await connection.run("BEGIN TRANSACTION");
138
118
  try {
139
119
  await (0, queryDB_js_1.default)(table, `CREATE OR REPLACE ${sourceTemporary ? "TEMP " : ""}TABLE ${source} AS
140
- SELECT ${columns.map((c) => `s.${q(c)}`).join(",")}, l.x AS "umapX", l.y AS "umapY"
120
+ SELECT ${prepared.sourceColumns.map((c) => `s.${q(c)}`).join(",")}, l.x AS "umapX", l.y AS "umapY"
141
121
  FROM ${snapshot} s JOIN ${names.rows} r ON s.${ordinal}=r.ordinal
142
122
  JOIN ${layout} l ON r.vertex=l.vertex ORDER BY s.${ordinal}`, (0, mergeOptions_js_1.default)(table, {
143
123
  table: table.name,
144
124
  method: "umap()",
145
- parameters: { column, options },
125
+ parameters: { columns, options },
146
126
  noClean: true,
147
127
  }));
148
128
  for (const sql of indexes)
@@ -158,5 +138,6 @@ async function execute(table, column, options) {
158
138
  for (const relation of scratch) {
159
139
  await connection.run(`DROP TABLE IF EXISTS ${relation}`);
160
140
  }
141
+ await prepared.cleanup();
161
142
  }
162
143
  }