@nshiab/simple-data-analysis-core 0.0.20 → 0.0.22

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.
Files changed (53) hide show
  1. package/esm/class/SimpleTable.d.ts +76 -21
  2. package/esm/class/SimpleTable.d.ts.map +1 -1
  3. package/esm/class/SimpleTable.js +103 -98
  4. package/esm/helpers/findGeoColumn.js +1 -1
  5. package/esm/helpers/getProjection.d.ts.map +1 -1
  6. package/esm/helpers/getProjection.js +3 -0
  7. package/esm/helpers/hasGeometryColumn.d.ts +20 -0
  8. package/esm/helpers/hasGeometryColumn.d.ts.map +1 -0
  9. package/esm/helpers/hasGeometryColumn.js +21 -0
  10. package/esm/helpers/writeGeoData.d.ts +9 -0
  11. package/esm/helpers/writeGeoData.d.ts.map +1 -0
  12. package/esm/helpers/writeGeoData.js +110 -0
  13. package/esm/methods/fuzzyClean.d.ts +2 -2
  14. package/esm/methods/fuzzyClean.d.ts.map +1 -1
  15. package/esm/methods/fuzzyClean.js +25 -6
  16. package/esm/methods/fuzzyJoin.d.ts +2 -2
  17. package/esm/methods/fuzzyJoin.d.ts.map +1 -1
  18. package/esm/methods/fuzzyJoin.js +3 -3
  19. package/esm/methods/fuzzyJoinQuery.d.ts +1 -1
  20. package/esm/methods/fuzzyJoinQuery.d.ts.map +1 -1
  21. package/esm/methods/fuzzyJoinQuery.js +13 -3
  22. package/esm/methods/loadSample.d.ts +9 -0
  23. package/esm/methods/loadSample.d.ts.map +1 -0
  24. package/esm/methods/loadSample.js +44 -0
  25. package/esm/methods/writeGeoDataQuery.d.ts.map +1 -1
  26. package/esm/methods/writeGeoDataQuery.js +4 -1
  27. package/package.json +1 -1
  28. package/script/class/SimpleTable.d.ts +76 -21
  29. package/script/class/SimpleTable.d.ts.map +1 -1
  30. package/script/class/SimpleTable.js +103 -98
  31. package/script/helpers/findGeoColumn.js +1 -1
  32. package/script/helpers/getProjection.d.ts.map +1 -1
  33. package/script/helpers/getProjection.js +3 -0
  34. package/script/helpers/hasGeometryColumn.d.ts +20 -0
  35. package/script/helpers/hasGeometryColumn.d.ts.map +1 -0
  36. package/script/helpers/hasGeometryColumn.js +24 -0
  37. package/script/helpers/writeGeoData.d.ts +9 -0
  38. package/script/helpers/writeGeoData.d.ts.map +1 -0
  39. package/script/helpers/writeGeoData.js +116 -0
  40. package/script/methods/fuzzyClean.d.ts +2 -2
  41. package/script/methods/fuzzyClean.d.ts.map +1 -1
  42. package/script/methods/fuzzyClean.js +25 -6
  43. package/script/methods/fuzzyJoin.d.ts +2 -2
  44. package/script/methods/fuzzyJoin.d.ts.map +1 -1
  45. package/script/methods/fuzzyJoin.js +3 -3
  46. package/script/methods/fuzzyJoinQuery.d.ts +1 -1
  47. package/script/methods/fuzzyJoinQuery.d.ts.map +1 -1
  48. package/script/methods/fuzzyJoinQuery.js +13 -3
  49. package/script/methods/loadSample.d.ts +9 -0
  50. package/script/methods/loadSample.d.ts.map +1 -0
  51. package/script/methods/loadSample.js +47 -0
  52. package/script/methods/writeGeoDataQuery.d.ts.map +1 -1
  53. package/script/methods/writeGeoDataQuery.js +4 -1
@@ -47,6 +47,7 @@ import normalizeQuery from "../methods/normalizeQuery.js";
47
47
  import rollingQuery from "../methods/rollingQuery.js";
48
48
  import distanceQuery from "../methods/distanceQuery.js";
49
49
  import getGeoData from "../methods/getGeoData.js";
50
+ import writeGeoData from "../helpers/writeGeoData.js";
50
51
  import splitSpread from "../methods/splitSpread.js";
51
52
  import { readdirSync } from "node:fs";
52
53
  import stringToArray from "../helpers/stringToArray.js";
@@ -54,24 +55,20 @@ import loadDataQuery from "../methods/loadDataQuery.js";
54
55
  import mergeOptions from "../helpers/mergeOptions.js";
55
56
  import queryDB from "../helpers/queryDB.js";
56
57
  import writeDataQuery from "../methods/writeDataQuery.js";
57
- import writeGeoDataQuery from "../methods/writeGeoDataQuery.js";
58
58
  import runQuery from "../helpers/runQuery.js";
59
59
  import aggregateGeoQuery from "../methods/aggregateGeoQuery.js";
60
60
  import summarize from "../methods/summarize.js";
61
61
  import correlations from "../methods/correlations.js";
62
62
  import linearRegressions from "../methods/linearRegressions.js";
63
63
  import joinGeo from "../methods/joinGeo.js";
64
- import shouldFlipBeforeExport from "../helpers/shouldFlipBeforeExport.js";
65
64
  import getProjection from "../helpers/getProjection.js";
66
65
  import cache from "../methods/cache.js";
67
66
  import camelCase from "../helpers/camelCase.js";
68
67
  import formatNumber from "../helpers/formatNumber.js";
69
68
  import createDirectory from "../helpers/createDirectory.js";
70
- import rewind from "../helpers/rewind.js";
71
69
  import writeDataAsArrays from "../helpers/writeDataAsArrays.js";
72
70
  import logData from "../helpers/logData.js";
73
71
  import fill from "../methods/fill.js";
74
- import { readFileSync, writeFileSync } from "node:fs";
75
72
  import loadArray from "../methods/loadArray.js";
76
73
  import cleanPath from "../helpers/cleanPath.js";
77
74
  import Simple from "./Simple.js";
@@ -88,16 +85,16 @@ import capitalizeQuery from "../methods/capitalizeQuery.js";
88
85
  import truncateQuery from "../methods/truncateQuery.js";
89
86
  import padQuery from "../methods/padQuery.js";
90
87
  import getProjectionParquet from "../helpers/getProjectionParquet.js";
88
+ import hasGeometryColumn from "../helpers/hasGeometryColumn.js";
91
89
  import unifyColumns from "../helpers/unifyColumns.js";
92
90
  import accumulateQuery from "../helpers/accumulateQuery.js";
93
- import stringifyDates from "../helpers/stringifyDates.js";
94
- import stringifyDatesInvert from "../helpers/stringifyDatesInvert.js";
95
91
  import unnestQuery from "../helpers/unnestQuery.js";
96
92
  import nestQuery from "../helpers/nestQuery.js";
97
93
  import concatenateRowQuery from "../helpers/concatenateRowQuery.js";
98
94
  import createFtsIndex from "../methods/createFtsIndex.js";
99
95
  import createVssIndex from "../methods/createVssIndex.js";
100
96
  import bm25 from "../methods/bm25.js";
97
+ import loadSample from "../methods/loadSample.js";
101
98
  import normalizeString from "../methods/normalizeString.js";
102
99
  /**
103
100
  * IMPORTANT: When extending this class, always use `this.sdb.newTable()` to
@@ -422,7 +419,13 @@ export default class SimpleTable extends Simple {
422
419
  *
423
420
  * @example
424
421
  * ```ts
425
- * // Load geospatial data from a shapefile and reproject to WGS84
422
+ * // Load geospatial data from a shapefile (with relevant files in the same folder) and reproject to WGS84
423
+ * await table.loadGeoData("./some-data/some-data.shp", { toWGS84: true });
424
+ * ```
425
+ *
426
+ * @example
427
+ * ```ts
428
+ * // Load geospatial data from a zipped shapefile and reproject to WGS84
426
429
  * await table.loadGeoData("./some-data.shp.zip", { toWGS84: true });
427
430
  * ```
428
431
  */
@@ -806,6 +809,32 @@ export default class SimpleTable extends Simple {
806
809
  }
807
810
  }
808
811
  }
812
+ /**
813
+ * Fetches sample data from the simple-data-analysis-core GitHub repository.
814
+ *
815
+ * @param sample - The name of the sample to load.
816
+ *
817
+ * Tabular data:
818
+ * - "fires": [firesCanada2023.csv](https://raw.githubusercontent.com/nshiab/simple-data-analysis-core/refs/heads/main/test/geodata/files/firesCanada2023.csv)
819
+ * - "recipes": [recipes.parquet](https://github.com/nshiab/simple-data-analysis-core/raw/refs/heads/main/test/data/files/recipes.parquet)
820
+ * - "temperatures": [dailyTemperatures.csv](https://raw.githubusercontent.com/nshiab/simple-data-analysis-core/refs/heads/main/test/data/files/dailyTemperatures.csv)
821
+ * - "temperaturesCities": [cities.csv](https://raw.githubusercontent.com/nshiab/simple-data-analysis-core/refs/heads/main/test/data/files/cities.csv)
822
+ *
823
+ * Geospatial data:
824
+ * - "canada": [CanadianProvincesAndTerritories.json](https://raw.githubusercontent.com/nshiab/simple-data-analysis-core/refs/heads/main/test/geodata/files/CanadianProvincesAndTerritories.json)
825
+ * - "firesGeo": [firesCanada2023.geojson](https://raw.githubusercontent.com/nshiab/simple-data-analysis-core/refs/heads/main/test/geodata/files/firesCanada2023.geojson)
826
+ *
827
+ * @category Importing Data
828
+ *
829
+ * @example
830
+ * ```ts
831
+ * // Load the fires sample data
832
+ * await table.loadSample("fires");
833
+ * ```
834
+ */
835
+ async loadSample(sample) {
836
+ return (await loadSample(this, sample));
837
+ }
809
838
  /**
810
839
  * Returns a new table with the same structure and data as this table. The data can be optionally filtered.
811
840
  * Note that cloning large tables can be a slow operation.
@@ -1903,48 +1932,57 @@ export default class SimpleTable extends Simple {
1903
1932
  * @param rightTable - The SimpleTable instance to be joined with this table.
1904
1933
  * @param leftColumn - The name of the column in this (left) table containing the text to compare.
1905
1934
  * @param rightColumn - The name of the column in the right table containing the text to compare.
1935
+ * @param threshold - The minimum similarity score (0–100) required for two rows to be joined. For `method: "ratio"`, a length-based pre-filter is automatically applied based on the threshold to improve performance without losing accuracy.
1906
1936
  * @param options - An optional object with configuration options:
1907
1937
  * @param options.method - The rapidfuzz similarity algorithm to use. Defaults to `"ratio"`.
1908
1938
  * - `"ratio"`: Overall similarity (Levenshtein-based).
1909
1939
  * - `"partial_ratio"`: Best partial/substring similarity.
1910
1940
  * - `"token_sort_ratio"`: Similarity after sorting tokens (words), useful for reordered words.
1911
1941
  * - `"token_set_ratio"`: Similarity based on sets of tokens, ignoring duplicates and word order.
1912
- * @param options.threshold - The minimum similarity score (0–100) required for two rows to be joined. Defaults to `80`.
1913
1942
  * @param options.similarityColumn - If provided, a column with this name is added to the result containing the similarity score (0–100). If omitted, the score is not included in the output.
1914
1943
  * @param options.outputTable - If `true`, the results will be stored in a new table with a generated name. If a string, it will be used as the name for the new table. If `false` or omitted, the current table will be overwritten. Defaults to `false`.
1944
+ * @param options.preFilterPrefixLen - An optional prefix length. Only strings sharing the same first N characters are compared. Note that prefix filtering is lossy (e.g. "John" vs. "Phon" will not match despite high similarity).
1915
1945
  * @returns A promise that resolves to a table instance containing the fuzzy-joined data (either the modified current table or a new table).
1916
1946
  * @category Table Operations
1917
1947
  *
1918
1948
  * @example
1919
1949
  * ```ts
1920
- * // Fuzzy left join tableA with tableB on 'name' (left) and 'standardName' (right) (ratio >= 80)
1921
- * await tableA.fuzzyJoin(tableB, "name", "standardName");
1950
+ * // Fuzzy left join tableA with tableB on 'name' (left) and 'standardName' (right) with a threshold of 80
1951
+ * // A length-based pre-filter is automatically applied.
1952
+ * await tableA.fuzzyJoin(tableB, "name", "standardName", 80);
1953
+ * ```
1954
+ *
1955
+ * @example
1956
+ * ```ts
1957
+ * // Fuzzy join with a prefix-based pre-filter and a threshold of 80
1958
+ * await tableA.fuzzyJoin(tableB, "name", "standardName", 80, {
1959
+ * preFilterPrefixLen: 3, // Must share the same first 3 characters
1960
+ * });
1922
1961
  * ```
1923
1962
  *
1924
1963
  * @example
1925
1964
  * ```ts
1926
1965
  * // Fuzzy join with a custom threshold and method, storing results in a new table
1927
- * const tableC = await tableA.fuzzyJoin(tableB, "name", "standardName", {
1966
+ * const tableC = await tableA.fuzzyJoin(tableB, "name", "standardName", 90, {
1928
1967
  * method: "token_sort_ratio",
1929
- * threshold: 90,
1930
1968
  * outputTable: "tableC",
1931
1969
  * });
1932
1970
  * ```
1933
1971
  *
1934
1972
  * @example
1935
1973
  * ```ts
1936
- * // Fuzzy join with a custom similarity column name
1937
- * await tableA.fuzzyJoin(tableB, "name", "standardName", {
1974
+ * // Fuzzy join with a custom similarity column name and a threshold of 80
1975
+ * await tableA.fuzzyJoin(tableB, "name", "standardName", 80, {
1938
1976
  * similarityColumn: "matchScore",
1939
1977
  * });
1940
1978
  * ```
1941
1979
  */
1942
- async fuzzyJoin(rightTable, leftColumn, rightColumn, options = {}) {
1980
+ async fuzzyJoin(rightTable, leftColumn, rightColumn, threshold, options = {}) {
1943
1981
  if (options.outputTable === true) {
1944
1982
  options.outputTable = `table${this.sdb.tableIncrement}`;
1945
1983
  this.sdb.tableIncrement += 1;
1946
1984
  }
1947
- return await fuzzyJoin(this, rightTable, leftColumn, rightColumn, options);
1985
+ return await fuzzyJoin(this, rightTable, leftColumn, rightColumn, threshold, options);
1948
1986
  }
1949
1987
  /**
1950
1988
  * Normalizes string values in a column by detecting fuzzy duplicates and replacing them with a single canonical value.
@@ -1958,42 +1996,52 @@ export default class SimpleTable extends Simple {
1958
1996
  *
1959
1997
  * @param column - The name of the column containing the strings to normalize.
1960
1998
  * @param newColumn - The name of the column to write the normalized values to. Use the same name as `column` to normalize in-place.
1999
+ * @param threshold - The minimum similarity score (0–100) for two strings to be considered duplicates. For `method: "ratio"`, a length-based pre-filter is automatically applied based on the threshold to improve performance without losing accuracy.
1961
2000
  * @param options - An optional object with configuration options:
1962
2001
  * @param options.method - The rapidfuzz similarity algorithm to use. Defaults to `"ratio"`.
1963
2002
  * - `"ratio"`: Overall similarity.
1964
2003
  * - `"partial_ratio"`: Best partial/substring similarity.
1965
2004
  * - `"token_sort_ratio"`: Similarity after sorting tokens (words), useful for reordered words.
1966
2005
  * - `"token_set_ratio"`: Similarity based on sets of tokens, ignoring duplicates and word order.
1967
- * @param options.threshold - The minimum similarity score (0–100) for two strings to be considered duplicates. Defaults to `80`.
1968
2006
  * @param options.keep - The strategy for choosing the canonical value within each cluster of similar strings. Defaults to `"mostCommon"`.
1969
2007
  * - `"mostCommon"`: Keep the value that appears most frequently in the original column.
1970
2008
  * - `"longestString"`: Keep the longest string in the cluster.
1971
2009
  * - `"shortestString"`: Keep the shortest string in the cluster.
1972
2010
  * - `"mostCentral"`: Keep the string with the highest total similarity score to all other cluster members (the most "central" string).
1973
2011
  * - `"maxScore"`: Keep the string that participates in the single highest-scoring pairwise match within the cluster.
2012
+ * @param options.preFilterPrefixLen - An optional prefix length. Only strings sharing the same first N characters are compared. Note that prefix filtering is lossy (e.g. "John" vs. "Phon" will not match despite high similarity).
1974
2013
  * @returns A promise that resolves when the column has been normalized.
1975
2014
  * @category Updating Data
1976
2015
  *
1977
2016
  * @example
1978
2017
  * ```ts
1979
- * // Normalize 'city' into a new 'cityClean' column, keeping the most common string per cluster
1980
- * await table.fuzzyClean("city", "cityClean");
2018
+ * // Normalize 'city' into a new 'cityClean' column, keeping the most common string per cluster with a threshold of 80
2019
+ * // A length-based pre-filter is automatically applied.
2020
+ * await table.fuzzyClean("city", "cityClean", 80);
2021
+ * ```
2022
+ *
2023
+ * @example
2024
+ * ```ts
2025
+ * // Normalize with a prefix-based pre-filter and a threshold of 80
2026
+ * await table.fuzzyClean("city", "cityClean", 80, {
2027
+ * preFilterPrefixLen: 5, // Must share the same first 5 characters
2028
+ * });
1981
2029
  * ```
1982
2030
  *
1983
2031
  * @example
1984
2032
  * ```ts
1985
- * // Normalize 'companyName' into a new column using token_sort_ratio and a stricter threshold
1986
- * await table.fuzzyClean("companyName", "companyNameClean", { method: "token_sort_ratio", threshold: 90 });
2033
+ * // Normalize 'companyName' into a new column using token_sort_ratio and a threshold of 90
2034
+ * await table.fuzzyClean("companyName", "companyNameClean", 90, { method: "token_sort_ratio" });
1987
2035
  * ```
1988
2036
  *
1989
2037
  * @example
1990
2038
  * ```ts
1991
- * // Normalize 'category' in-place, keeping the longest string in each cluster
1992
- * await table.fuzzyClean("category", "category", { keep: "longestString" });
2039
+ * // Normalize 'category' in-place, keeping the longest string in each cluster and a threshold of 80
2040
+ * await table.fuzzyClean("category", "category", 80, { keep: "longestString" });
1993
2041
  * ```
1994
2042
  */
1995
- async fuzzyClean(column, newColumn, options = {}) {
1996
- await fuzzyClean(this, column, newColumn, options);
2043
+ async fuzzyClean(column, newColumn, threshold, options = {}) {
2044
+ await fuzzyClean(this, column, newColumn, threshold, options);
1997
2045
  }
1998
2046
  /**
1999
2047
  * Replaces specified strings in the selected columns.
@@ -3925,6 +3973,9 @@ export default class SimpleTable extends Simple {
3925
3973
  * ```
3926
3974
  */
3927
3975
  async getData(options = {}) {
3976
+ if (await hasGeometryColumn(this)) {
3977
+ throw new Error("Table contains geometry columns. Use getGeoData() instead.");
3978
+ }
3928
3979
  const columns = options.columns
3929
3980
  ? (typeof options.columns === "string"
3930
3981
  ? [options.columns]
@@ -3989,8 +4040,9 @@ export default class SimpleTable extends Simple {
3989
4040
  * ```
3990
4041
  */
3991
4042
  async points(columnLat, columnLon, newColumn) {
3992
- await queryDB(this, `INSTALL spatial; LOAD spatial;
3993
- ALTER TABLE "${this.name}" ADD COLUMN "${newColumn}" GEOMETRY; UPDATE "${this.name}" SET "${newColumn}" = ST_Point2D("${columnLat}", "${columnLon}")`, mergeOptions(this, {
4043
+ await queryDB(this, (await this.getColumns()).includes(newColumn)
4044
+ ? `INSTALL spatial; LOAD spatial; UPDATE "${this.name}" SET "${newColumn}" = ST_Point2D("${columnLat}", "${columnLon}")`
4045
+ : `INSTALL spatial; LOAD spatial; ALTER TABLE "${this.name}" ADD COLUMN "${newColumn}" GEOMETRY; UPDATE "${this.name}" SET "${newColumn}" = ST_Point2D("${columnLat}", "${columnLon}")`, mergeOptions(this, {
3994
4046
  table: this.name,
3995
4047
  method: "points()",
3996
4048
  parameters: { columnLat, columnLon, newColumn },
@@ -4406,7 +4458,9 @@ export default class SimpleTable extends Simple {
4406
4458
  const column = typeof options.column === "string"
4407
4459
  ? options.column
4408
4460
  : await findGeoColumn(this);
4409
- await queryDB(this, `ALTER TABLE "${this.name}" ADD "${newColumn}" GEOMETRY; UPDATE "${this.name}" SET "${newColumn}" = ST_Buffer("${column}", ${distance});`, mergeOptions(this, {
4461
+ await queryDB(this, (await this.getColumns()).includes(newColumn)
4462
+ ? `INSTALL spatial; LOAD spatial; UPDATE "${this.name}" SET "${newColumn}" = ST_Buffer("${column}", ${distance})`
4463
+ : `INSTALL spatial; LOAD spatial; ALTER TABLE "${this.name}" ADD "${newColumn}" GEOMETRY; UPDATE "${this.name}" SET "${newColumn}" = ST_Buffer("${column}", ${distance})`, mergeOptions(this, {
4410
4464
  table: this.name,
4411
4465
  method: "buffer()",
4412
4466
  parameters: { column, newColumn, distance },
@@ -4492,7 +4546,9 @@ export default class SimpleTable extends Simple {
4492
4546
  if (this.projections[column1] !== this.projections[column2]) {
4493
4547
  throw new Error(`${column1} and ${column2} don't have the same projection.\n${column1}: ${this.projections[column1]}\n${column2}: ${this.projections[column2]}`);
4494
4548
  }
4495
- await queryDB(this, `ALTER TABLE "${this.name}" ADD "${newColumn}" GEOMETRY; UPDATE "${this.name}" SET "${newColumn}" = ST_Intersection("${column1}", "${column2}")`, mergeOptions(this, {
4549
+ await queryDB(this, (await this.getColumns()).includes(newColumn)
4550
+ ? `INSTALL spatial; LOAD spatial; UPDATE "${this.name}" SET "${newColumn}" = ST_Intersection("${column1}", "${column2}")`
4551
+ : `INSTALL spatial; LOAD spatial; ALTER TABLE "${this.name}" ADD "${newColumn}" GEOMETRY; UPDATE "${this.name}" SET "${newColumn}" = ST_Intersection("${column1}", "${column2}")`, mergeOptions(this, {
4496
4552
  table: this.name,
4497
4553
  method: "intersection()",
4498
4554
  parameters: { column1, column2, newColumn },
@@ -4617,7 +4673,9 @@ export default class SimpleTable extends Simple {
4617
4673
  if (this.projections[column1] !== this.projections[column2]) {
4618
4674
  throw new Error(`${column1} and ${column2} don't have the same projection.\n${column1}: ${this.projections[column1]}\n${column2}: ${this.projections[column2]}`);
4619
4675
  }
4620
- await queryDB(this, `ALTER TABLE "${this.name}" ADD "${newColumn}" GEOMETRY; UPDATE "${this.name}" SET "${newColumn}" = ST_Union("${column1}", "${column2}")`, mergeOptions(this, {
4676
+ await queryDB(this, (await this.getColumns()).includes(newColumn)
4677
+ ? `INSTALL spatial; LOAD spatial; UPDATE "${this.name}" SET "${newColumn}" = ST_Union("${column1}", "${column2}")`
4678
+ : `INSTALL spatial; LOAD spatial; ALTER TABLE "${this.name}" ADD "${newColumn}" GEOMETRY; UPDATE "${this.name}" SET "${newColumn}" = ST_Union("${column1}", "${column2}")`, mergeOptions(this, {
4621
4679
  table: this.name,
4622
4680
  method: "union()",
4623
4681
  parameters: { column1, column2, newColumn },
@@ -4708,7 +4766,9 @@ export default class SimpleTable extends Simple {
4708
4766
  const column = typeof options.column === "string"
4709
4767
  ? options.column
4710
4768
  : await findGeoColumn(this);
4711
- await queryDB(this, `ALTER TABLE "${this.name}" ADD "${newColumn}" GEOMETRY; UPDATE "${this.name}" SET "${newColumn}" = ST_Centroid("${column}");`, mergeOptions(this, {
4769
+ await queryDB(this, (await this.getColumns()).includes(newColumn)
4770
+ ? `INSTALL spatial; LOAD spatial; UPDATE "${this.name}" SET "${newColumn}" = ST_Centroid("${column}")`
4771
+ : `INSTALL spatial; LOAD spatial; ALTER TABLE "${this.name}" ADD "${newColumn}" GEOMETRY; UPDATE "${this.name}" SET "${newColumn}" = ST_Centroid("${column}")`, mergeOptions(this, {
4712
4772
  table: this.name,
4713
4773
  method: "centroid()",
4714
4774
  parameters: { column, newColumn },
@@ -4993,6 +5053,9 @@ export default class SimpleTable extends Simple {
4993
5053
  * ```
4994
5054
  */
4995
5055
  async writeData(file, options = {}) {
5056
+ if (await hasGeometryColumn(this)) {
5057
+ throw new Error("Table contains geometry columns. Use writeGeoData() instead.");
5058
+ }
4996
5059
  createDirectory(file);
4997
5060
  const extension = getExtension(file);
4998
5061
  if (options.dataAsArrays) {
@@ -5007,12 +5070,12 @@ export default class SimpleTable extends Simple {
5007
5070
  }
5008
5071
  }
5009
5072
  /**
5010
- * Writes the table's geospatial data to a file in GeoJSON or GeoParquet format.
5073
+ * Writes the table's geospatial data to a file in GeoJSON, GeoParquet, or Shapefile format.
5011
5074
  * If the specified path does not exist, it will be created.
5012
5075
  *
5013
5076
  * For GeoJSON files (`.geojson` or `.json`), if the projection is WGS84 or EPSG:4326 (`[latitude, longitude]` axis order), the coordinates will be flipped to follow the RFC7946 standard (`[longitude, latitude]` axis order) in the output.
5014
5077
  *
5015
- * @param file - The absolute path to the output file (e.g., `"./output.geojson"`, `"./output.geoparquet"`).
5078
+ * @param file - The absolute path to the output file (e.g., `"./output.geojson"`, `"./output.geoparquet"`, `"./shapefile-folder/output.shp"`).
5016
5079
  * @param options - An optional object with configuration options:
5017
5080
  * @param options.precision - For GeoJSON, the maximum number of figures after the decimal separator to write in coordinates. Defaults to `undefined` (full precision).
5018
5081
  * @param options.compression - For GeoParquet, if `true`, the output will be ZSTD compressed. Defaults to `false`.
@@ -5036,6 +5099,12 @@ export default class SimpleTable extends Simple {
5036
5099
  *
5037
5100
  * @example
5038
5101
  * ```ts
5102
+ * // Write geospatial data to a Shapefile with all relevant files in the same folder
5103
+ * await table.writeGeoData("./shapefile-folder/output.shp");
5104
+ * ```
5105
+ *
5106
+ * @example
5107
+ * ```ts
5039
5108
  * // Write GeoJSON with specific precision and metadata
5040
5109
  * await table.writeGeoData("./output_high_precision.geojson", {
5041
5110
  * precision: 6,
@@ -5044,71 +5113,7 @@ export default class SimpleTable extends Simple {
5044
5113
  * ```
5045
5114
  */
5046
5115
  async writeGeoData(file, options = {}) {
5047
- createDirectory(file);
5048
- const fileExtension = getExtension(file);
5049
- if (fileExtension === "geojson" || fileExtension === "json") {
5050
- let types;
5051
- if (options.formatDates === true) {
5052
- types = await this.getTypes();
5053
- if (Object.values(types).includes("DATE") ||
5054
- Object.values(types).includes("TIMESTAMP")) {
5055
- await stringifyDates(this, types);
5056
- }
5057
- }
5058
- if (typeof options.compression === "boolean") {
5059
- throw new Error("The compression option is not supported for writing GeoJSON files.");
5060
- }
5061
- const geoColumn = await findGeoColumn(this);
5062
- const flip = shouldFlipBeforeExport(this.projections[geoColumn]);
5063
- if (flip) {
5064
- await this.flipCoordinates(geoColumn);
5065
- await queryDB(this, writeGeoDataQuery(this.name, file, fileExtension, options), mergeOptions(this, {
5066
- table: this.name,
5067
- method: "writeGeoData()",
5068
- parameters: { file, options },
5069
- }));
5070
- await this.flipCoordinates(geoColumn);
5071
- }
5072
- else {
5073
- await queryDB(this, writeGeoDataQuery(this.name, file, fileExtension, options), mergeOptions(this, {
5074
- table: this.name,
5075
- method: "writeGeoData()",
5076
- parameters: { file, options },
5077
- }));
5078
- }
5079
- if (options.metadata) {
5080
- const fileData = JSON.parse(readFileSync(file, "utf-8"));
5081
- fileData.metadata = options.metadata;
5082
- writeFileSync(file, JSON.stringify(fileData));
5083
- }
5084
- if (options.rewind) {
5085
- const fileData = JSON.parse(readFileSync(file, "utf-8"));
5086
- const fileRewinded = rewind(fileData);
5087
- writeFileSync(file, JSON.stringify(fileRewinded));
5088
- }
5089
- if (types && (Object.values(types).includes("DATE") ||
5090
- Object.values(types).includes("TIMESTAMP"))) {
5091
- await stringifyDatesInvert(this, types);
5092
- }
5093
- }
5094
- else if (fileExtension === "geoparquet") {
5095
- if (typeof options.precision === "number") {
5096
- throw new Error("The precision option is not supported for writing PARQUET files. Use the .reducePrecision() method.");
5097
- }
5098
- if (typeof options.rewind === "boolean") {
5099
- throw new Error("The rewind option is not supported for writing PARQUET files.");
5100
- }
5101
- await queryDB(this, `COPY "${this.name}" TO '${cleanPath(file)}' WITH (FORMAT PARQUET${options.compression === true ? ", COMPRESSION 'zstd'" : ""}, KV_METADATA {
5102
- projections: '${JSON.stringify(this.projections)}'
5103
- });`, mergeOptions(this, {
5104
- table: this.name,
5105
- method: "writeGeoData()",
5106
- parameters: { file, options },
5107
- }));
5108
- }
5109
- else {
5110
- throw new Error(`Unknown extension ${fileExtension}`);
5111
- }
5116
+ await writeGeoData(this, file, options);
5112
5117
  }
5113
5118
  /**
5114
5119
  * Caches the results of computations in `./.sda-cache`.
@@ -3,7 +3,7 @@ export default async function findGeoColumn(SimpleTable) {
3
3
  const types = await SimpleTable.getTypes();
4
4
  const geometries = Object.values(types).filter((d) => d.toLowerCase() === "geometry");
5
5
  if (geometries.length === 0) {
6
- throw new Error("No column storing geometries");
6
+ throw new Error("Table contains no geometry columns.");
7
7
  }
8
8
  else if (geometries.length > 1) {
9
9
  throw new Error("More than one column storing geometries. If the method allows to specify one, do it. Otherwise, use the selectColumns methods beforehand.");
@@ -1 +1 @@
1
- {"version":3,"file":"getProjection.d.ts","sourceRoot":"","sources":["../../src/helpers/getProjection.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,QAAQ,MAAM,sBAAsB,CAAC;AAGjD,wBAA8B,aAAa,CACzC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,mBA8Bb"}
1
+ {"version":3,"file":"getProjection.d.ts","sourceRoot":"","sources":["../../src/helpers/getProjection.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,QAAQ,MAAM,sBAAsB,CAAC;AAGjD,wBAA8B,aAAa,CACzC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,mBAiCb"}
@@ -15,6 +15,9 @@ export default async function getProjection(simpleDB, file) {
15
15
  throw new Error("No queryResults");
16
16
  }
17
17
  const proj4 = queryResult[0].proj4;
18
+ if (proj4 === null) {
19
+ return "UNKNOWN";
20
+ }
18
21
  if (typeof proj4 !== "string") {
19
22
  throw new Error(`Expected proj4 to be a string, got ${typeof proj4}`);
20
23
  }
@@ -0,0 +1,20 @@
1
+ import type SimpleTable from "../class/SimpleTable.js";
2
+ /**
3
+ * Returns `true` if the table has one or more columns of type geometry.
4
+ *
5
+ * Uses `getTypes()` to inspect column types and checks for any column
6
+ * whose type normalizes to `"geometry"`.
7
+ *
8
+ * @param table - The SimpleTable instance to inspect.
9
+ * @returns `true` if at least one geometry column exists, `false` otherwise.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const hasGeo = await hasGeometryColumn(table);
14
+ * if (hasGeo) {
15
+ * console.log("This table contains geometry columns");
16
+ * }
17
+ * ```
18
+ */
19
+ export default function hasGeometryColumn(table: SimpleTable): Promise<boolean>;
20
+ //# sourceMappingURL=hasGeometryColumn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hasGeometryColumn.d.ts","sourceRoot":"","sources":["../../src/helpers/hasGeometryColumn.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAEvD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAA8B,iBAAiB,CAC7C,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,OAAO,CAAC,CAGlB"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Returns `true` if the table has one or more columns of type geometry.
3
+ *
4
+ * Uses `getTypes()` to inspect column types and checks for any column
5
+ * whose type normalizes to `"geometry"`.
6
+ *
7
+ * @param table - The SimpleTable instance to inspect.
8
+ * @returns `true` if at least one geometry column exists, `false` otherwise.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const hasGeo = await hasGeometryColumn(table);
13
+ * if (hasGeo) {
14
+ * console.log("This table contains geometry columns");
15
+ * }
16
+ * ```
17
+ */
18
+ export default async function hasGeometryColumn(table) {
19
+ const types = await table.getTypes();
20
+ return Object.values(types).some((t) => t.toLowerCase() === "geometry");
21
+ }
@@ -0,0 +1,9 @@
1
+ import type SimpleTable from "../class/SimpleTable.js";
2
+ export default function writeGeoData(table: SimpleTable, file: string, options?: {
3
+ precision?: number;
4
+ compression?: boolean;
5
+ rewind?: boolean;
6
+ metadata?: unknown;
7
+ formatDates?: boolean;
8
+ }): Promise<void>;
9
+ //# sourceMappingURL=writeGeoData.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writeGeoData.d.ts","sourceRoot":"","sources":["../../src/helpers/writeGeoData.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAEvD,wBAA8B,YAAY,CACxC,KAAK,EAAE,WAAW,EAClB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IACP,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;CAClB,GACL,OAAO,CAAC,IAAI,CAAC,CAqIf"}
@@ -0,0 +1,110 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import createDirectory from "./createDirectory.js";
3
+ import getExtension from "./getExtension.js";
4
+ import findGeoColumn from "./findGeoColumn.js";
5
+ import hasGeometryColumn from "./hasGeometryColumn.js";
6
+ import shouldFlipBeforeExport from "./shouldFlipBeforeExport.js";
7
+ import queryDB from "./queryDB.js";
8
+ import writeGeoDataQuery from "../methods/writeGeoDataQuery.js";
9
+ import mergeOptions from "./mergeOptions.js";
10
+ import rewind from "./rewind.js";
11
+ import stringifyDates from "./stringifyDates.js";
12
+ import stringifyDatesInvert from "./stringifyDatesInvert.js";
13
+ import cleanPath from "./cleanPath.js";
14
+ export default async function writeGeoData(table, file, options = {}) {
15
+ createDirectory(file);
16
+ if (!(await hasGeometryColumn(table))) {
17
+ throw new Error("Table contains no geometry columns. Use writeData() instead.");
18
+ }
19
+ const fileExtension = getExtension(file);
20
+ if (fileExtension === "geojson" || fileExtension === "json") {
21
+ let types;
22
+ if (options.formatDates === true) {
23
+ types = await table.getTypes();
24
+ if (Object.values(types).includes("DATE") ||
25
+ Object.values(types).includes("TIMESTAMP")) {
26
+ await stringifyDates(table, types);
27
+ }
28
+ }
29
+ if (typeof options.compression === "boolean") {
30
+ throw new Error("The compression option is not supported for writing GeoJSON files.");
31
+ }
32
+ const geoColumn = await findGeoColumn(table);
33
+ const flip = shouldFlipBeforeExport(table.projections[geoColumn]);
34
+ if (flip) {
35
+ await table.flipCoordinates(geoColumn);
36
+ await queryDB(table, writeGeoDataQuery(table.name, file, fileExtension, options), mergeOptions(table, {
37
+ table: table.name,
38
+ method: "writeGeoData()",
39
+ parameters: { file, options },
40
+ }));
41
+ await table.flipCoordinates(geoColumn);
42
+ }
43
+ else {
44
+ await queryDB(table, writeGeoDataQuery(table.name, file, fileExtension, options), mergeOptions(table, {
45
+ table: table.name,
46
+ method: "writeGeoData()",
47
+ parameters: { file, options },
48
+ }));
49
+ }
50
+ if (options.metadata) {
51
+ const fileData = JSON.parse(readFileSync(file, "utf-8"));
52
+ fileData.metadata = options.metadata;
53
+ writeFileSync(file, JSON.stringify(fileData));
54
+ }
55
+ if (options.rewind) {
56
+ const fileData = JSON.parse(readFileSync(file, "utf-8"));
57
+ const fileRewinded = rewind(fileData);
58
+ writeFileSync(file, JSON.stringify(fileRewinded));
59
+ }
60
+ if (types && (Object.values(types).includes("DATE") ||
61
+ Object.values(types).includes("TIMESTAMP"))) {
62
+ await stringifyDatesInvert(table, types);
63
+ }
64
+ }
65
+ else if (fileExtension === "shp") {
66
+ if (typeof options.precision === "number" ||
67
+ typeof options.compression === "boolean" ||
68
+ typeof options.rewind === "boolean" ||
69
+ options.metadata ||
70
+ options.formatDates === true) {
71
+ throw new Error("The following options are not supported for writing SHAPEFILE files: precision, compression, rewind, metadata, and formatDates.");
72
+ }
73
+ const geoColumn = await findGeoColumn(table);
74
+ const flip = shouldFlipBeforeExport(table.projections[geoColumn]);
75
+ if (flip) {
76
+ await table.flipCoordinates(geoColumn);
77
+ await queryDB(table, writeGeoDataQuery(table.name, file, fileExtension, options), mergeOptions(table, {
78
+ table: table.name,
79
+ method: "writeGeoData()",
80
+ parameters: { file, options },
81
+ }));
82
+ await table.flipCoordinates(geoColumn);
83
+ }
84
+ else {
85
+ await queryDB(table, writeGeoDataQuery(table.name, file, fileExtension, options), mergeOptions(table, {
86
+ table: table.name,
87
+ method: "writeGeoData()",
88
+ parameters: { file, options },
89
+ }));
90
+ }
91
+ }
92
+ else if (fileExtension === "geoparquet") {
93
+ if (typeof options.precision === "number") {
94
+ throw new Error("The precision option is not supported for writing PARQUET files. Use the .reducePrecision() method.");
95
+ }
96
+ if (typeof options.rewind === "boolean") {
97
+ throw new Error("The rewind option is not supported for writing PARQUET files.");
98
+ }
99
+ await queryDB(table, `COPY "${table.name}" TO '${cleanPath(file)}' WITH (FORMAT PARQUET${options.compression === true ? ", COMPRESSION 'zstd'" : ""}, KV_METADATA {
100
+ projections: '${JSON.stringify(table.projections)}'
101
+ });`, mergeOptions(table, {
102
+ table: table.name,
103
+ method: "writeGeoData()",
104
+ parameters: { file, options },
105
+ }));
106
+ }
107
+ else {
108
+ throw new Error(`Unknown extension ${fileExtension}`);
109
+ }
110
+ }
@@ -1,7 +1,7 @@
1
1
  import type SimpleTable from "../class/SimpleTable.js";
2
- export default function fuzzyClean(table: SimpleTable, column: string, newColumn: string, options?: {
2
+ export default function fuzzyClean(table: SimpleTable, column: string, newColumn: string, threshold: number, options?: {
3
3
  method?: "ratio" | "partial_ratio" | "token_sort_ratio" | "token_set_ratio";
4
- threshold?: number;
5
4
  keep?: "mostCommon" | "longestString" | "shortestString" | "mostCentral" | "maxScore";
5
+ preFilterPrefixLen?: number;
6
6
  }): Promise<void>;
7
7
  //# sourceMappingURL=fuzzyClean.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fuzzyClean.d.ts","sourceRoot":"","sources":["../../src/methods/fuzzyClean.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAIvD,wBAA8B,UAAU,CACtC,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IACP,MAAM,CAAC,EACH,OAAO,GACP,eAAe,GACf,kBAAkB,GAClB,iBAAiB,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EACD,YAAY,GACZ,eAAe,GACf,gBAAgB,GAChB,aAAa,GACb,UAAU,CAAC;CACX,GACL,OAAO,CAAC,IAAI,CAAC,CAyNf"}
1
+ {"version":3,"file":"fuzzyClean.d.ts","sourceRoot":"","sources":["../../src/methods/fuzzyClean.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAIvD,wBAA8B,UAAU,CACtC,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IACP,MAAM,CAAC,EACH,OAAO,GACP,eAAe,GACf,kBAAkB,GAClB,iBAAiB,CAAC;IACtB,IAAI,CAAC,EACD,YAAY,GACZ,eAAe,GACf,gBAAgB,GAChB,aAAa,GACb,UAAU,CAAC;IACf,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACxB,GACL,OAAO,CAAC,IAAI,CAAC,CAmPf"}