battlelog/lib/JSONSearch/jsonSearch.ts

183 lines
4.2 KiB
TypeScript

export type JSONObject = { [key: string]: any };
export type JSONArray = JSONObject[];
export function searchJSON(json: string | JSONObject | JSONArray, query: string): string | JSONArray | JSONObject | string[] | undefined {
if (typeof json === 'string') {
json = JSON.parse(json);
}
const props = query.split('.');
const currentArray: JSONArray = Array.isArray(json) ? json : [json];
for (let i = 0; i < currentArray.length; i++) {
let current = currentArray[i];
for (let j = 0; j < props.length; j++) {
const prop = props[j];
// let isArrayQuery = false;
let arrayProp: string, arrayQuery: string | string[], arrayIndex: number;
if (prop.includes('[') && prop.includes(']')) {
// isArrayQuery = true;
[arrayProp, arrayQuery] = prop.split(/[\[\]]/g).filter(Boolean);
if (arrayQuery.includes('/')) {
arrayQuery = arrayQuery.split('/');
} else {
arrayQuery = [arrayQuery];
}
arrayIndex = 0;
current = current[arrayProp][arrayIndex];
while (current) {
if (checkArrayQuery(current, arrayQuery)) {
break;
}
current = current[arrayProp][++arrayIndex];
}
} else {
current = current[prop];
}
if (current === undefined) {
break;
}
}
if (current !== undefined) {
return current;
}
}
return undefined;
}
function checkArrayQuery(obj: JSONObject, queryArr: string[]): boolean {
return queryArr.every((query) => {
let [prop, operator, valueStr] = query.split(/([=!><]+)/);
prop = obj[prop];
const values = valueStr.split('/');
if (values.length > 1) {
return values.includes(prop);
}
switch (operator) {
case'=':
return areSimilar(prop, valueStr);
case '==':
return prop == valueStr;
case '!=':
return prop != valueStr;
case '>':
return prop > valueStr;
case '>=':
return prop >= valueStr;
case '<':
return prop < valueStr;
case '<=':
return prop <= valueStr;
case '><':
return includes(prop, valueStr);
default:
return false;
}
});
}
function areSimilar<T extends string | number>(val1: T, val2: T, tolerance = .05, useRatio?: boolean): boolean {
if ((typeof val1 === 'number' || isNaN(Number(val1))) && (typeof val2 === 'number' || isNaN(Number(val2)))) {
switch (!useRatio || val2 == 0) {
case true: {
return Math.abs(Number(val1) - Number(val2)) < tolerance;
}
case false: {
const ratio = Number(val1) / (Number(val2))
return ratio >= 1 - tolerance && ratio <= 1 + tolerance;
}
}
}
const v1 = val1.toString();
const v2 = val2.toString();
const dist = levenshtein(v1, v2);
const diff = dist / ((v1.length + v2.length) / 2);
return useRatio ? diff <= 1 + tolerance && diff >= 1 - tolerance : diff <= Math.round(tolerance);
}
function levenshtein(str1: string, str2: string): number {
const cost: number[][] = [];
const n = str1.length;
const m = str2.length;
let i, j;
const minimum = (a: number, b: number, c: number) => {
let min = a;
if (b < min) {
min = b;
}
if (c < min) {
min = c;
}
return min;
}
if (n == 0) {
return Infinity;
}
if (m == 0) {
return Infinity;
}
for (let i = 0; i <= n; i++) {
cost[i] = [];
}
for (i = 0; i <= n; i++) {
cost[i][0] = i;
}
for (j = 0; j <= m; j++) {
cost[0][j] = j;
}
for (i = 1; i <= n; i++) {
const x = str1.charAt(i - 1);
for (j = 1; j <= m; j++) {
const y = str2.charAt(j - 1);
if (x == y) {
cost[i][j] = cost[i - 1][j - 1];
} else {
cost[i][j] = 1 + minimum(cost[i - 1][j - 1], cost[i][j - 1], cost[i - 1][j]);
}
}//endfor
}//endfor
return cost[n][m];
}
function includes<T extends string | number>(val1: T | T[], val2: T) {
if (Array.isArray(val1)) return val1.includes(val2);
else {
const v1 = val1.toString();
const v2 = val2.toString();
return v1.includes(v2);
}
}
function normalize(val: any) {
if (typeof val === 'string') return val.normalize();
return val;
}