311 lines
9.1 KiB
JavaScript
311 lines
9.1 KiB
JavaScript
const utils = require('./utils');
|
|
|
|
class TableIndex {
|
|
//opts.type = 'string' || 'number' || 'number_as_string'
|
|
constructor(opts = {}) {
|
|
const type = opts.type || 'string';
|
|
this.depth = opts.depth || 11;
|
|
this.allowUndef = opts.allowUndef || false;
|
|
this.unique = opts.unique || false;
|
|
|
|
this.hash = new Map();
|
|
this.sorted = [[]];
|
|
this.delCount = 0;
|
|
|
|
this.isNumber = (type === 'number' || type === 'number_as_string');
|
|
this.numberAsString = (type === 'number_as_string');
|
|
this.valueAsString = !this.isNumber || this.numberAsString;
|
|
|
|
this.cmp = (a, b) => a.localeCompare(b);
|
|
if (type === 'number') {
|
|
this.cmp = (a, b) => a - b;
|
|
} else if (type === 'number_as_string') {
|
|
this.cmp = (a, b) => (a < b ? -1 : (a > b ? 1 : 0));
|
|
}
|
|
}
|
|
|
|
checkType(v) {
|
|
if (typeof(v) != 'number' && this.isNumber)
|
|
throw new Error(`Indexed value must be a number, got type:${typeof(v)}, value:${v}`);
|
|
|
|
if (typeof(v) != 'string' && !this.isNumber)
|
|
throw new Error(`Indexed value must be a string, got type:${typeof(v)}, value:${v}`);
|
|
}
|
|
|
|
prepareValue(v) {
|
|
let result = v;
|
|
if (this.numberAsString) {
|
|
result = v.toString().padStart(this.depth, '0');
|
|
}
|
|
if (this.valueAsString && result.length > this.depth)
|
|
result = result.substring(0, this.depth);
|
|
return result;
|
|
}
|
|
|
|
add(value, id) {
|
|
if (value === undefined && this.allowUndef)
|
|
return;
|
|
|
|
this.checkType(value);
|
|
|
|
value = this.prepareValue(value);
|
|
if (this.hash.has(value)) {
|
|
if (this.unique) {
|
|
const id_ = this.hash.get(value);
|
|
if (id_ !== id) {
|
|
throw new Error(`Collision for unique index detected: value:${value}, id1:${id_}, id2:${id}`);
|
|
}
|
|
} else {
|
|
const ids = this.hash.get(value);
|
|
ids.add(id);
|
|
}
|
|
} else {
|
|
if (this.unique) {
|
|
this.hash.set(value, id);
|
|
} else {
|
|
const ids = new Set();
|
|
this.hash.set(value, ids);
|
|
ids.add(id);
|
|
}
|
|
|
|
let s = this.sorted.length - 1;
|
|
const d = this.sorted[s];
|
|
d.push(value);
|
|
|
|
let i = d.length - 1;
|
|
//вставка
|
|
while (i > 0 && this.cmp(d[i], d[i - 1]) < 0) {
|
|
const v = d[i];
|
|
d[i] = d[i - 1];
|
|
d[i - 1] = v;
|
|
i--;
|
|
}
|
|
|
|
if (d.length > 10) {
|
|
//слияние
|
|
while (s > 0 && this.sorted[s].length >= this.sorted[s - 1].length) {
|
|
const a = this.sorted.pop();
|
|
const b = this.sorted.pop();
|
|
const c = [];
|
|
let i = 0;
|
|
let j = 0;
|
|
while (i < a.length || j < b.length) {
|
|
if (i < a.length && (j === b.length || this.cmp(a[i], b[j]) <= 0)) {
|
|
c.push(a[i]);
|
|
i++;
|
|
}
|
|
if (j < b.length && (i === a.length || this.cmp(b[j], a[i]) <= 0)) {
|
|
c.push(b[j]);
|
|
j++;
|
|
}
|
|
}
|
|
this.sorted.push(c);
|
|
s--;
|
|
}
|
|
|
|
this.sorted.push([]);
|
|
}
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
del(value, id, forceClean = false) {
|
|
if (value === undefined && this.allowUndef)
|
|
return;
|
|
|
|
this.checkType(value);
|
|
|
|
value = this.prepareValue(value);
|
|
if (this.hash.has(value)) {
|
|
if (this.unique) {
|
|
const id_ = this.hash.get(value);
|
|
if (id_ === id) {
|
|
this.hash.delete(value);
|
|
this.delCount++;
|
|
}
|
|
} else {
|
|
const ids = this.hash.get(value);
|
|
|
|
ids.delete(id);
|
|
|
|
if (!ids.size) {
|
|
this.hash.delete(value);
|
|
this.delCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.delCount > (this.sorted[0].length >> 2) || forceClean) {
|
|
for (let s = 0; s < this.sorted.length; s++) {
|
|
const a = this.sorted[s];
|
|
const b = [];
|
|
for (let i = 0; i < a.length; i++) {
|
|
if (this.hash.has(a[i]))
|
|
b.push(a[i]);
|
|
}
|
|
this.sorted[s] = b;
|
|
}
|
|
|
|
this.sorted = this.sorted.filter(a => a.length);
|
|
if (!this.sorted.length) {
|
|
this.sorted = [[]]
|
|
} else {
|
|
this.sorted.sort((a, b) => b.length - a.length);
|
|
}
|
|
|
|
this.delCount = 0;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
reduce(from, to) {
|
|
const useFrom = (from !== undefined);
|
|
const useTo = (to !== undefined);
|
|
|
|
if (useFrom) {
|
|
this.checkType(from);
|
|
from = this.prepareValue(from);
|
|
}
|
|
if (useTo) {
|
|
this.checkType(to);
|
|
to = this.prepareValue(to);
|
|
}
|
|
|
|
const result = [];
|
|
for (let s = 0; s < this.sorted.length; s++) {
|
|
const a = this.sorted[s];
|
|
if (!a.length) // на всякий случай
|
|
continue;
|
|
|
|
let leftIndex = 0;
|
|
if (useFrom) {
|
|
//дихотомия
|
|
let left = 0;
|
|
let right = a.length - 1;
|
|
while (left < right) {
|
|
let mid = left + ((right - left) >> 1);
|
|
if (this.cmp(from, a[mid]) <= 0)
|
|
right = mid;
|
|
else
|
|
left = mid + 1;
|
|
}
|
|
|
|
leftIndex = right;
|
|
if (this.cmp(from, a[right]) > 0)
|
|
leftIndex++;
|
|
}
|
|
|
|
let rightIndex = a.length;
|
|
if (useTo) {
|
|
//дихотомия
|
|
let left = 0;
|
|
let right = a.length - 1;
|
|
while (left < right) {
|
|
let mid = right - ((right - left) >> 1);
|
|
if (this.cmp(to, a[mid]) >= 0)
|
|
left = mid;
|
|
else
|
|
right = mid - 1;
|
|
}
|
|
|
|
rightIndex = left;
|
|
if (this.cmp(to, a[left]) >= 0)
|
|
rightIndex++;
|
|
}
|
|
//console.log(a, leftIndex, rightIndex);
|
|
if (this.unique) {
|
|
const ids = new Set();
|
|
for (let i = leftIndex; i < rightIndex; i++) {
|
|
const value = a[i];
|
|
if (this.hash.has(value)) {
|
|
ids.add(this.hash.get(value));
|
|
}
|
|
}
|
|
result.push(ids);
|
|
} else {
|
|
for (let i = leftIndex; i < rightIndex; i++) {
|
|
const value = a[i];
|
|
if (this.hash.has(value)) {
|
|
result.push(this.hash.get(value));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return utils.unionSet(result);
|
|
}
|
|
|
|
min() {
|
|
let result = new Set();
|
|
|
|
let min = null;
|
|
let id = null;
|
|
for (let s = 0; s < this.sorted.length; s++) {
|
|
const a = this.sorted[s];
|
|
if (!a.length) // на всякий случай
|
|
continue;
|
|
if (a[0] < min || min === null) {
|
|
min = a[0];
|
|
id = this.hash.get(min);
|
|
}
|
|
}
|
|
|
|
if (id !== null) {
|
|
if (this.unique)
|
|
result.add(id);
|
|
else
|
|
result = id;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
max() {
|
|
let result = new Set();
|
|
|
|
let max = null;
|
|
let id = null;
|
|
for (let s = 0; s < this.sorted.length; s++) {
|
|
const a = this.sorted[s];
|
|
if (!a.length) // на всякий случай
|
|
continue;
|
|
|
|
const last = a.length - 1;
|
|
if (a[last] > max || max === null) {
|
|
max = a[last];
|
|
id = this.hash.get(max);
|
|
}
|
|
}
|
|
|
|
if (id !== null) {
|
|
if (this.unique)
|
|
result.add(id);
|
|
else
|
|
result = id;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
iter(checkFunc) {
|
|
const result = new Set();
|
|
for (const [value, ids] of this.hash.entries()) {
|
|
const checkResult = checkFunc(value);
|
|
if (checkResult === undefined)
|
|
break;
|
|
if (checkResult) {
|
|
if (this.unique) {
|
|
result.add(ids);
|
|
} else {
|
|
for (const id of ids)
|
|
result.add(id);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
module.exports = TableIndex; |