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(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(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; }