qvdjs 2.2.2 → 2.2.4

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.
package/README.md CHANGED
@@ -42,28 +42,31 @@ thing worth reading:
42
42
 
43
43
  ## Checked against Qlik Sense
44
44
 
45
- qvdjs is compared with Qlik Sense itself, not only with its own reading of the format. 36 fields in
46
- 7 datasets - integers, doubles, text (non-ASCII, empty and number-like included), booleans, dates,
47
- timestamps and NULLs - are defined twice, as Qlik load-script expressions and as the JavaScript
48
- values a caller would write. Qlik Sense and qvdjs each write them to a QVD, and three things are
49
- checked:
45
+ qvdjs is compared with Qlik Sense itself, not only with its own reading of the format. 48 fields in
46
+ 8 datasets - integers, doubles, text (non-ASCII, empty and number-like included), booleans, dates,
47
+ timestamps, and NULLs beside numbers, text, dates and timestamps - are defined twice, as Qlik
48
+ load-script expressions and as the JavaScript values a caller would write. Qlik Sense and qvdjs each
49
+ write them to a QVD, and three things are checked:
50
50
 
51
51
  - **qvdjs writes the content Qlik writes** - the same symbols, of the same types, with the same text,
52
52
  on the same rows.
53
- - **Given Qlik's tags and number formats, qvdjs writes them unchanged** - the bit layout, below, is
54
- the only part of a field's header left to differ.
53
+ - **Given Qlik's tags and number formats, qvdjs writes them unchanged** - the bit layout and a tag the
54
+ values contradict, below, are the only parts of a field's header left to differ.
55
55
  - **Qlik reads qvdjs's file as it reads its own** - loaded into Qlik and stored again, with `Text()`,
56
56
  `Num()`, `IsNum()`, `IsText()`, `IsNull()` and `Year()` the same on every row.
57
57
 
58
58
  The live comparison runs on demand against a Qlik Sense server; its last run, against Qlik Sense
59
- Enterprise on Windows build 50699, passed for all 36 fields. Qlik's files from that run are kept in
59
+ Enterprise on Windows build 50699, passed for all 48 fields. Qlik's files from that run are kept in
60
60
  the test suite, so **every CI run compares qvdjs's output with them again** - on Linux, Windows and
61
61
  macOS, under Node 22, 24 and 26 - along with reference files Qlik wrote for booleans, dates and
62
62
  timestamps.
63
63
 
64
- Two differences are by design: Qlik often gives a field more bits in the index than its values need,
65
- where qvdjs uses the fewest, and qvdjs adds no tags or number formats of its own - Qlik works tags out
66
- when it loads a file. QVD is also QlikView's format, but QlikView is not part of the comparison.
64
+ Three differences are by design. Qlik often gives a field more bits in the index than its values need,
65
+ where qvdjs uses the fewest. qvdjs adds no tags or number formats of its own - Qlik works tags out when
66
+ it loads a file, and keeps any the file carries. And qvdjs drops a tag the values contradict, even one
67
+ Qlik wrote: Qlik tags dates outside 1980 to 2080 as text when their field has no date format, although
68
+ its own `IsNum()` reads every one as a number. QVD is also QlikView's format, but QlikView is not part
69
+ of the comparison.
67
70
 
68
71
  → [Checked against Qlik Sense](https://qvdjs.ptarmiganlabs.com/v2.0/overview/checked-against-qlik-sense/)
69
72
 
package/dist/index.cjs CHANGED
@@ -1201,6 +1201,212 @@ var init_bitUtils = __esm({
1201
1201
  }
1202
1202
  });
1203
1203
 
1204
+ // src/util/seenNumbers.js
1205
+ function firstJudgementRow(rows) {
1206
+ return Math.min(WINDOW_ROWS, Math.max(MIN_JUDGED_LOOKUPS, Math.floor(rows / 2)));
1207
+ }
1208
+ function slotOf(value, mask) {
1209
+ const product = value * MULTIPLIER;
1210
+ return product < EXACT && product > -EXACT ? product & mask : ((product | 0) ^ (product * UPPER_HALF | 0)) & mask;
1211
+ }
1212
+ function slotsFor(count, most) {
1213
+ let slots = MIN_SLOTS;
1214
+ while (slots < count && slots < most) {
1215
+ slots *= 2;
1216
+ }
1217
+ return slots;
1218
+ }
1219
+ function shareOf(columns) {
1220
+ let slots = MAX_SLOTS;
1221
+ while (slots > MIN_SLOTS && slots * columns > TOTAL_SLOTS) {
1222
+ slots /= 2;
1223
+ }
1224
+ return slots;
1225
+ }
1226
+ function symbolPassTable(rows, pool) {
1227
+ return pool.take(MIN_SLOTS) ? new SeenNumbers(MIN_SLOTS, slotsFor(rows, MAX_SLOTS), SYMBOL_PASS_MISS_LIMIT, pool) : null;
1228
+ }
1229
+ function numericCellsAfter(data, column, stopped, numbers) {
1230
+ const most = stopped.cells + data.length - stopped.row;
1231
+ if (!indexPassWants(numbers, most)) {
1232
+ return most;
1233
+ }
1234
+ let cells = stopped.cells;
1235
+ for (let row = stopped.row; row < data.length; row++) {
1236
+ if (typeof data[row]?.[column] === "number") {
1237
+ cells++;
1238
+ }
1239
+ }
1240
+ return cells;
1241
+ }
1242
+ function numbersIn(map) {
1243
+ let numbers = 0;
1244
+ for (const key of map.keys()) {
1245
+ if (typeof key === "number") {
1246
+ numbers++;
1247
+ }
1248
+ }
1249
+ return numbers;
1250
+ }
1251
+ function indexPassWants(numbers, cells) {
1252
+ return numbers > 0 && numbers <= INDEX_PASS_MISS_LIMIT * cells;
1253
+ }
1254
+ function indexPassTable(numbers, cells, symbols, share, pool) {
1255
+ const slots = Math.min(slotsFor(numbers * SLOTS_PER_NUMBER, share), slotsFor(cells, share));
1256
+ return pool.take(slots) ? new SeenNumbers(slots, slots, INDEX_PASS_MISS_LIMIT - numbers / cells, pool, symbols) : null;
1257
+ }
1258
+ function endWindow(tables, row = 0, stopped = null) {
1259
+ for (let column = 0; column < tables.length; column++) {
1260
+ const table = tables[column];
1261
+ if (table === null || table === NO_TABLE_YET) {
1262
+ continue;
1263
+ }
1264
+ const lookups = table.cells - table.judgedCells;
1265
+ if (lookups < MIN_JUDGED_LOOKUPS) {
1266
+ continue;
1267
+ }
1268
+ if (table.misses > (table.judged ? table.limit : FIRST_WINDOW_MISS_LIMIT) * lookups) {
1269
+ tables[column] = null;
1270
+ table.pool.give(table.values.length);
1271
+ if (stopped !== null) {
1272
+ stopped[column] = { row, cells: table.cells };
1273
+ }
1274
+ } else {
1275
+ table.judgedCells = table.cells;
1276
+ table.misses = 0;
1277
+ table.judged = true;
1278
+ }
1279
+ }
1280
+ }
1281
+ var MULTIPLIER, EXACT, UPPER_HALF, WINDOW_ROWS, MIN_JUDGED_LOOKUPS, MIN_SLOTS, MAX_SLOTS, TOTAL_SLOTS, SLOTS_PER_NUMBER, FIRST_WINDOW_MISS_LIMIT, SYMBOL_PASS_MISS_LIMIT, INDEX_PASS_MISS_LIMIT, SlotPool, SeenNumbers, NO_TABLE_YET;
1282
+ var init_seenNumbers = __esm({
1283
+ "src/util/seenNumbers.js"() {
1284
+ MULTIPLIER = 2654435761;
1285
+ EXACT = 2 ** 53;
1286
+ UPPER_HALF = 2 ** -32;
1287
+ WINDOW_ROWS = 16384;
1288
+ MIN_JUDGED_LOOKUPS = 1024;
1289
+ __name(firstJudgementRow, "firstJudgementRow");
1290
+ MIN_SLOTS = 64;
1291
+ MAX_SLOTS = 65536;
1292
+ TOTAL_SLOTS = 2 ** 21;
1293
+ SLOTS_PER_NUMBER = 8;
1294
+ FIRST_WINDOW_MISS_LIMIT = 0.9;
1295
+ SYMBOL_PASS_MISS_LIMIT = 0.3;
1296
+ INDEX_PASS_MISS_LIMIT = 0.75;
1297
+ __name(slotOf, "slotOf");
1298
+ __name(slotsFor, "slotsFor");
1299
+ __name(shareOf, "shareOf");
1300
+ SlotPool = class {
1301
+ static {
1302
+ __name(this, "SlotPool");
1303
+ }
1304
+ /**
1305
+ * @param {number} [slots] How many there are to give.
1306
+ */
1307
+ constructor(slots = TOTAL_SLOTS) {
1308
+ this.left = slots;
1309
+ }
1310
+ /**
1311
+ * Takes slots, if there are that many left.
1312
+ *
1313
+ * @param {number} slots How many.
1314
+ * @return {boolean} Whether they were taken.
1315
+ */
1316
+ take(slots) {
1317
+ if (slots > this.left) {
1318
+ return false;
1319
+ }
1320
+ this.left -= slots;
1321
+ return true;
1322
+ }
1323
+ /**
1324
+ * Gives slots back.
1325
+ *
1326
+ * @param {number} slots How many.
1327
+ */
1328
+ give(slots) {
1329
+ this.left += slots;
1330
+ }
1331
+ };
1332
+ SeenNumbers = class _SeenNumbers {
1333
+ static {
1334
+ __name(this, "SeenNumbers");
1335
+ }
1336
+ /**
1337
+ * @param {number} slots How many slots: a power of two from `MIN_SLOTS` to `MAX_SLOTS`, already taken from
1338
+ * `pool`.
1339
+ * @param {number} maxSlots The most it may grow to, in the symbol table pass.
1340
+ * @param {number} limit The share of its lookups a judged window may miss.
1341
+ * @param {SlotPool} pool Where its slots came from, and where they go back when it is switched off.
1342
+ * @param {number} [symbols] In the index table pass, the column's symbols, one bit each. -1, the default, in
1343
+ * the symbol table pass.
1344
+ */
1345
+ constructor(slots, maxSlots, limit, pool, symbols = -1) {
1346
+ this.values = new Float64Array(slots).fill(NaN);
1347
+ this.indices = new Int32Array(symbols >= 0 ? slots : 0);
1348
+ this.met = new Uint32Array(symbols >= 0 ? Math.ceil(symbols / 32) : 0);
1349
+ this.mask = slots - 1;
1350
+ this.maxSlots = maxSlots;
1351
+ this.limit = limit;
1352
+ this.pool = pool;
1353
+ this.cells = 0;
1354
+ this.judgedCells = 0;
1355
+ this.misses = 0;
1356
+ this.judged = symbols >= 0;
1357
+ this.inserted = 0;
1358
+ }
1359
+ /**
1360
+ * Remembers a number the symbol table pass has just put in the map, growing the table instead once more
1361
+ * than an eighth of its slots would hold distinct numbers - if its pool has the slots. A table the pool
1362
+ * cannot grow stops trying, and keeps remembering in the slots it has.
1363
+ *
1364
+ * @param {number} slot The number's slot, from `slotOf`.
1365
+ * @param {number} value The number.
1366
+ * @return {SeenNumbers} The table to use from now on: this one, or a larger, empty one.
1367
+ */
1368
+ remember(slot, value) {
1369
+ this.inserted++;
1370
+ if (this.inserted * SLOTS_PER_NUMBER > this.values.length && this.values.length < this.maxSlots) {
1371
+ const slots = Math.min(this.values.length * 4, this.maxSlots);
1372
+ if (this.pool.take(slots - this.values.length)) {
1373
+ const grown = new _SeenNumbers(slots, this.maxSlots, this.limit, this.pool);
1374
+ grown.inserted = this.inserted;
1375
+ grown.cells = this.cells;
1376
+ grown.judgedCells = this.judgedCells;
1377
+ grown.misses = this.misses;
1378
+ grown.judged = this.judged;
1379
+ return grown;
1380
+ }
1381
+ this.maxSlots = this.values.length;
1382
+ }
1383
+ this.values[slot] = value;
1384
+ return this;
1385
+ }
1386
+ /**
1387
+ * Marks a symbol met through this table, in the index table pass.
1388
+ *
1389
+ * @param {number} index The symbol the map answered.
1390
+ * @return {boolean} Whether it had been met before; false for a first sight.
1391
+ */
1392
+ metBefore(index) {
1393
+ const word = index >>> 5;
1394
+ const bit = 1 << (index & 31);
1395
+ const before = (this.met[word] & bit) !== 0;
1396
+ this.met[word] |= bit;
1397
+ return before;
1398
+ }
1399
+ };
1400
+ NO_TABLE_YET = new SeenNumbers(1, 1, 0, new SlotPool(1));
1401
+ __name(symbolPassTable, "symbolPassTable");
1402
+ __name(numericCellsAfter, "numericCellsAfter");
1403
+ __name(numbersIn, "numbersIn");
1404
+ __name(indexPassWants, "indexPassWants");
1405
+ __name(indexPassTable, "indexPassTable");
1406
+ __name(endWindow, "endWindow");
1407
+ }
1408
+ });
1409
+
1204
1410
  // src/QvdFileWriter.js
1205
1411
  var QvdFileWriter_exports = {};
1206
1412
  __export(QvdFileWriter_exports, {
@@ -1488,6 +1694,7 @@ var init_QvdFileWriter = __esm({
1488
1694
  init_cellRules();
1489
1695
  init_symbolBytes();
1490
1696
  init_storedSymbols();
1697
+ init_seenNumbers();
1491
1698
  __name(notXmlIndex, "notXmlIndex");
1492
1699
  __name(checkHeaderText, "checkHeaderText");
1493
1700
  __name(checkHeaderTexts, "checkHeaderTexts");
@@ -1950,7 +2157,14 @@ var init_QvdFileWriter = __esm({
1950
2157
  const byCells = state.map((column) => column.byCell);
1951
2158
  const byObjects = state.map((column) => column.byObject);
1952
2159
  const containsNull = columns.map(() => false);
2160
+ const pool = new SlotPool();
2161
+ const seen = columns.map(() => NO_TABLE_YET);
2162
+ const firstJudgement = firstJudgementRow(numRows);
2163
+ const stopped = columns.map(() => null);
1953
2164
  for (let row = 0; row < numRows; row++) {
2165
+ if (row % WINDOW_ROWS === 0 && row !== 0 || row === firstJudgement) {
2166
+ endWindow(seen, row, stopped);
2167
+ }
1954
2168
  const values = data[row];
1955
2169
  if (values !== null && values !== void 0 && !Array.isArray(values)) {
1956
2170
  throw new exports.QvdValidationError("Each row must be an array of values", {
@@ -1981,7 +2195,34 @@ var init_QvdFileWriter = __esm({
1981
2195
  }
1982
2196
  continue;
1983
2197
  }
2198
+ let table = null;
2199
+ let slot = 0;
2200
+ if (typeof value === "number") {
2201
+ table = seen[column];
2202
+ if (table !== null) {
2203
+ table.cells++;
2204
+ slot = slotOf(value, table.mask);
2205
+ if (table.values[slot] === value) {
2206
+ continue;
2207
+ }
2208
+ table.misses++;
2209
+ if (table === NO_TABLE_YET) {
2210
+ table = symbolPassTable(numRows, pool);
2211
+ seen[column] = table;
2212
+ if (table === null) {
2213
+ stopped[column] = { row, cells: 0 };
2214
+ } else {
2215
+ table.cells = 1;
2216
+ table.misses = 1;
2217
+ slot = slotOf(value, table.mask);
2218
+ }
2219
+ }
2220
+ }
2221
+ }
1984
2222
  if (byCells[column].has(value)) {
2223
+ if (table !== null) {
2224
+ table.values[slot] = value;
2225
+ }
1985
2226
  continue;
1986
2227
  }
1987
2228
  if (typeof value === "number") {
@@ -2001,6 +2242,9 @@ var init_QvdFileWriter = __esm({
2001
2242
  refuseCell(value, columns[column], row, this._path);
2002
2243
  }
2003
2244
  byCells[column].set(value, slotForCell(state[column], value, row, this._path));
2245
+ if (table !== null && typeof value === "number") {
2246
+ seen[column] = table.remember(slot, value);
2247
+ }
2004
2248
  }
2005
2249
  }
2006
2250
  const columnBuffers = [];
@@ -2009,10 +2253,12 @@ var init_QvdFileWriter = __esm({
2009
2253
  const { keys, texts } = state[column];
2010
2254
  const kinds = new Uint8Array(keys.length);
2011
2255
  let byteLength = 0;
2256
+ let numberKeys = 0;
2012
2257
  for (let slot = 0; slot < keys.length; slot++) {
2013
2258
  const key = keys[slot];
2014
2259
  const text = texts[slot];
2015
2260
  if (typeof key === "number") {
2261
+ numberKeys++;
2016
2262
  if (numberProblem(key) !== null) {
2017
2263
  checkNumber(key, null, { column: columns[column], file: this._path, stage: "buildSymbolTable" });
2018
2264
  }
@@ -2041,10 +2287,20 @@ var init_QvdFileWriter = __esm({
2041
2287
  this._symbolTableMetadata?.push([symbolsOffset, byteLength, containsNull[column]]);
2042
2288
  this._symbolCounts?.push(keys.length);
2043
2289
  this._symbolFacts?.push(state[column].facts);
2290
+ const numbers = state[column].byCell === state[column].byKey ? numberKeys : numbersIn(state[column].byCell);
2291
+ const table = seen[column];
2044
2292
  this._symbolIndexByValue?.push({
2045
2293
  byCell: state[column].byCell,
2046
2294
  byKey: state[column].byKey,
2047
- byObject: state[column].byObject
2295
+ byObject: state[column].byObject,
2296
+ numbers,
2297
+ cells: table === NO_TABLE_YET ? 0 : table !== null ? table.cells : numericCellsAfter(
2298
+ data,
2299
+ column,
2300
+ /** @type {{row: number, cells: number}} */
2301
+ stopped[column],
2302
+ numbers
2303
+ )
2048
2304
  });
2049
2305
  symbolsOffset += byteLength;
2050
2306
  this._emitProgress("symbol-table", column + 1, numColumns);
@@ -2094,7 +2350,7 @@ var init_QvdFileWriter = __esm({
2094
2350
  const maxStoredIndex = symbolCount === 0 ? 0 : symbolCount - 1 + nullShift;
2095
2351
  const bitWidth = maxStoredIndex === 0 ? 0 : 32 - Math.clz32(maxStoredIndex);
2096
2352
  const lookup = this._symbolIndexByValue[column];
2097
- const maps = lookup.byCell === lookup.byKey ? [lookup.byKey, lookup.byObject] : Object.values(lookup);
2353
+ const maps = lookup.byCell === lookup.byKey ? [lookup.byKey, lookup.byObject] : [lookup.byCell, lookup.byKey, lookup.byObject];
2098
2354
  for (const map of maps) {
2099
2355
  for (const index of map.values()) {
2100
2356
  if (index + nullShift > maxStoredIndex) {
@@ -2120,14 +2376,44 @@ var init_QvdFileWriter = __esm({
2120
2376
  const byCells = this._symbolIndexByValue.map((lookup) => lookup.byCell);
2121
2377
  const byKeys = this._symbolIndexByValue.map((lookup) => lookup.byKey);
2122
2378
  const byObjects = this._symbolIndexByValue.map((lookup) => lookup.byObject);
2379
+ const symbolCounts = this._symbolCounts;
2380
+ const wanted = this._symbolIndexByValue.map((lookup) => indexPassWants(lookup.numbers, lookup.cells));
2381
+ const share = shareOf(wanted.filter(Boolean).length);
2382
+ const pool = new SlotPool();
2383
+ const seen = this._symbolIndexByValue.map(
2384
+ (lookup, column) => wanted[column] ? indexPassTable(lookup.numbers, lookup.cells, symbolCounts[column], share, pool) : null
2385
+ );
2386
+ const firstJudgement = firstJudgementRow(numRows);
2123
2387
  for (let row = 0, recordBase = 0; row < numRows; row++, recordBase += recordByteSize) {
2388
+ if (row % WINDOW_ROWS === 0 && row !== 0 || row === firstJudgement) {
2389
+ endWindow(seen);
2390
+ }
2124
2391
  const values = data[row];
2125
2392
  for (let column = 0; column < numColumns; column++) {
2126
2393
  const value = values?.[column];
2127
2394
  if (value === null || value === void 0) {
2128
2395
  continue;
2129
2396
  }
2130
- const index = typeof value === "object" ? byObjects[column].get(value) ?? byKeys[column].get(value.number) : byCells[column].get(value);
2397
+ const table = typeof value === "number" ? seen[column] : null;
2398
+ let index;
2399
+ if (table !== null) {
2400
+ table.cells++;
2401
+ const slot = slotOf(value, table.mask);
2402
+ if (table.values[slot] === value) {
2403
+ index = table.indices[slot];
2404
+ } else {
2405
+ index = byCells[column].get(value);
2406
+ if (index !== void 0) {
2407
+ table.values[slot] = value;
2408
+ table.indices[slot] = index;
2409
+ if (table.metBefore(index)) {
2410
+ table.misses++;
2411
+ }
2412
+ }
2413
+ }
2414
+ } else {
2415
+ index = typeof value === "object" ? byObjects[column].get(value) ?? byKeys[column].get(value.number) : byCells[column].get(value);
2416
+ }
2131
2417
  if (index === void 0) {
2132
2418
  throw new exports.QvdValidationError("A value is missing from the symbol table", {
2133
2419
  field: columns[column],