export class DACTAL { constructor(data = {}) { this.data = data; this.index = {}; this.index_modified = new Set; this.adapters = {}; this.adaptive = false; this.features = { // earnest magic you can disable autoresolve: true, // if dataset X exists, following prop X to a literal is treated as an ID lookup plurality: true, // prop and props may be used interchangeably inlinemath: true, // traverse literals starting with = do inline math, like .[=score/total] unscore: true, // props_like_this can also be referred to as props like this guessid: true, // items without ids may have them inferred from their names guessname: true // items without names may have them inferred from other properties } this.data['query history'] = []; this.savedquerynames = new Set(); this.debug = false; this.recache = false; this.timelimit = 120000; this.statusf = (statusmsg) => { if (statusmsg) console.log(statusmsg) }; this.internal_datasets = ['queries', 'query history', 'connectors', 'adapters', 'annotators', 'current results', 'data updates', 'assistance', '.clf API routes']; this.annotators = { group: (item) => item.of, label: (item) => item.of, as: (item) => item.of, count: (item) => this.vals(item).length, total: (item) => this.numvals(item).reduce((acc, val) => acc + val, 0), average: (item) => this.numvals(item).reduce((acc, val) => acc + val, 0) / this.numvals(item).length, median: (item) => { const nums = this.numvals(item).sort(); return nums[Math.floor(nums.length / 2)]; }, min: (item) => { const nums = this.numvals(item); return nums.length == 0 ? [] : Math.min(...nums) }, max: (item) => { const nums = this.numvals(item); return nums.length == 0 ? [] : Math.max(...nums) }, product: (item) => this.numvals(item).reduce((acc, val) => acc * val, 1), difference: (item) => this.numvals(item).reduce((acc, val) => acc - val), quotient: (item) => this.numvals(item).reduce((acc, val) => acc / val), percent: (item) => Math.round(this.numvals(item).reduce((acc, val) => acc / val) * 100), sqrt: (item) => Math.sqrt(this.numvals(item)[0]), log: (item) => Math.log(this.numvals(item)[0]), log10: (item) => Math.log10(this.numvals(item)[0]), abs: (item) => Math.abs(this.numvals(item)[0]), is: (item) => (item.of?.length > 0) ? 1 : 0, isnt: (item) => (item.of?.length === 0) ? 1 : 0, yesno: (item) => (item.of?.length > 0) ? 'yes' : 'no', missing: (item) => (item.of?.length === 0) ? true : [], otherwise: (item) => { const ikeys = this.kkeys(item); if (item.of?.length > 0 || ikeys.length == 0) { return item.of; } else { return item[ikeys[0]]; } }, sortsame: (item) => { const sortforms = this.vals(item).map((v) => v?.toString()?.toLowerCase()?.replace(/^the /, '')); const sortformset = new Set(sortforms); return (sortformset.size == 1 ? 'sortsame' : []); }, startsame: (item) => { const vals = this.vals(item); const shortest = vals.sort((a, b) => a.length - b.length)[0]; return (vals.filter((v) => v.startsWith(shortest)).length == vals.length ? shortest : []); }, concatenate: (item) => this.vals(item).join(' '), join: (item) => item.of.map(this.getname).join(this.vals(item)[0]), str: (item) => item.of.map(this.getname).join(''), 'to json': (item) => JSON.stringify(item.of), quote: (item) => `“${this.vals(item)[0]}”`, url: (item) => { let u = item.of.map(this.getname).join(''); if (!u.startsWith('https://')) u = 'https://' + u; for (const prop of this.kkeys(item)) { const val = item[prop]; (Array.isArray(val) ? val : [val]).forEach((vv) => u = u + (u.match(/\?/) ? '&' : '?') + encodeURIComponent(prop) + '=' + encodeURIComponent(vv)); } return u; }, remove: (item) => this.vals(item).reduce((acc, val) => acc.replaceAll(val, '')), replace: (item) => { return item.of.map((i) => { let newi = this.getname(i); this.kkeys(item).map((k) => { newi = newi.replaceAll(k, item[k].toString()); }) return newi; }); }, matches: (item) => { const matchers = item.match.map((m) => m.toLowerCase()); return matchers.filter((m) => item.text.find((i) => this.getname(i).toLowerCase().includes(m))); }, bmk: (item) => this.vals(item).map((v) => v.toString().toLowerCase().replaceAll(/[\$\xA2-\xA5\u058F\u060B\u09F2\u09F3\u09FB\u0AF1\u0BF9\u0E3F\u17DB\u20A0-\u20BD\uA838\uFDFC\uFE69\uFF04\uFFE0\uFFE1\uFFE5\uFFE6,+]/g, '').replaceAll('b', 'kkk').replaceAll('m', 'kk').replaceAll('k', '000')), sortform: (item) => this.vals(item).map((val) => val?.toString()?.toLowerCase()?.replace(/^the /, '')), zip: (item) => { const itemkeys = this.kkeys(item); const zipped = []; for (let i = 0; i < item[itemkeys[0]].length; i++) { const zipline = {}; for (const key of itemkeys) { zipline[key] = item[key][i]; } zipped.push(zipline); } return zipped; }, pairs: (item) => { return item.of.slice(0, -1).map((val, vx) => ({pair: [val, item.of[vx + 1]]})); }, triples: (item) => { return item.of.slice(0, -2).map((val, vx) => ({triple: [val, item.of[vx + 1], item.of[vx + 2]]})); }, quads: (item) => { return item.of.slice(0, -3).map((val, vx) => ({quad: [val, item.of[vx + 1], item.of[vx + 2], item.of[vx + 3]]})); }, sequences: (item) => { return item.of.map((val, valx, vallist) => ({sequence: vallist.slice(0, valx + 1).map((val) => this.dcopy(val))})); }, split: (item) => { const itemvals = this.vals(item); let splitter; let tobesplit; if (Object.keys(item).length == 1) itemvals.push(' '); if (itemvals.length == 1) { splitter = itemvals[0]; tobesplit = item.of.slice(0); } else { splitter = itemvals.pop(); tobesplit = itemvals.slice(0); } if (splitter.startsWith('~')) splitter = new RegExp(splitter.replace(/^~*/, ''), splitter.startsWith('~~') ? 'i' : ''); return tobesplit.flatMap((v) => v.toString().split(splitter)); }, unpack: (item) => { return item.of.flatMap((val) => { let valstr = val.toString(); return this.vals(item).reduce((acc, size, x) => { acc.push(valstr.substring(0, size)); valstr = valstr.substring(size); return acc; }, []); }); }, extract: (item) => { const itemvals = this.vals(item); const delimiters = itemvals.pop(); const res = []; for (const itemval of (itemvals.length > 0 ? itemvals : item.of)) { if (delimiters.startsWith('~')) { itemval.matchAll(new RegExp(delimiters.slice(1), 'g')).forEach((match) => { if (match.groups) { res.push(match.groups); } else { match.slice(1).forEach((m) => res.push(m)); } }); } else { for (let i = 0; i < delimiters.length; i += 2) { const d1 = delimiters[i]; const d2 = delimiters[i + 1]; const d1x = itemval.indexOf(d1); const d2x = itemval.indexOf(d2); if (d1x > -1 && d2x > d1x) { res.push(itemval.slice(d1x + 1, d2x).trim()); break; } } } } return res; }, unchain: (item) => { const chainprops = this.kkeys(item); const unchained = []; const queue = item.of.slice(0); while (queue.length > 0) { const thisitem = queue.shift(); if (!unchained.includes(thisitem)) { unchained.push(thisitem); for (const chainprop of chainprops) { if (this.dtype(thisitem, 'object') && chainprop in thisitem) { if (this.dtype(thisitem[chainprop], 'array')) { for (const x of thisitem[chainprop].slice(0).reverse()) queue.unshift(x); } else if (thisitem[chainprop]) { queue.unshift(thisitem[chainprop]) } } } } } return unchained; }, itemize: (item) => { const propname = item?.property ?? 'property'; const valname = item?.value ?? 'value'; return item.of.flatMap((subitem) => Object.entries(subitem).map(([key, val]) => ({ [propname]: key, [valname]: val }))); }, schematize: (item) => { const itemkeys = this.kkeys(item); const schematized = {}; item[itemkeys[0]].forEach((subitem) => { let subkey; let subval; if (this.dtype(subitem, 'array')) { const [subkey, subval] = subitem; } else { [subkey, subval] = Object.values(subitem); } schematized[subkey] = subval; }); return schematized; }, index: (item) => { return Object.entries(item).filter(([k, v]) => k != 'of').map(([k, v]) => ({ id: k, name: !isNaN(v) ? Number(v) : v })); }, unflatten: (item) => { const itemkeys = this.kkeys(item); if (itemkeys.length === 0) itemkeys.push(''); const newindex = {}; const neworder = []; item.of.forEach((subitem) => { Object.keys(subitem).forEach((field) => { itemkeys.forEach((key) => { if (field.startsWith(key)) { const subid = field.replace(key, ''); if (subid.length > 0) { if (!(subid in newindex)) { newindex[subid] = {}; neworder.push(subid); } newindex[subid][key] = subitem[field]; } } }); }); }); itemkeys.forEach((key) => delete item[key]); return neworder.map((k) => { const subitem = {}; subitem.subid = k; Object.assign(subitem, newindex[k]); return subitem; }); }, detupled: (item) => { const newobj = {}; item.of.forEach((subitem) => { if (Array.isArray(subitem) && subitem.length == 2) { newobj[subitem[0]] = subitem[1]; } }) return [newobj]; }, csv: (item) => { const keys = Object.entries(item).find(([k, v]) => k != 'of')[1]; const res = []; const vals = item.of.flatMap((val) => this.dtype(val, 'string') ? val.split('\n').map((val) => val.trim()) : val); for (let i = 0; i < vals.length; i += keys.length) { const newitem = {}; for (let k = 0; k < keys.length; k++) { newitem[keys[k]] = vals[i + k]; } res.push(newitem); } return res; }, tsv: (item) => { const text = this.getname(item); const lines = text.split('\n').filter((line) => line != '').map((line) => line.split('\t').map((val) => val.trim())); const keys = lines[0]; return lines.slice(1).map((vals) => Object.fromEntries(vals.map((val, vi) => [keys[vi], val]))); }, ssv: (item) => { const text = this.getname(item); const lines = text.split('\n').filter((line) => line != '').map((line) => line.split(/ +/).map((val) => val.trim())); const keys = lines[0]; return lines.slice(1).map((vals) => Object.fromEntries(vals.map((val, vi) => [keys[vi], val]))); }, json: (item) => this.vals(item).flatMap((v) => JSON.parse(v)), year: (item) => { if (item.date?.toString()?.length > 0) return item.date?.[0]?.toString()?.match(/\d\d\d\d/)?.[0]; let yearmatch = this.getname(item.toString()).match(/\d\d\d\d/); if (yearmatch) { return yearmatch[0]; } else { yearmatch = this.getid(item.of[0] || '').toString().match(/\d\d\d\d/); if (yearmatch) { return yearmatch[0]; } return null; } }, month: (item) => { if (item.date?.length > 0) return item.date[0].match(/\d\d\d\d-(\d\d)-\d\d/)[1]; let monthmatch = this.getname(item).match(/\d\d\d\d-(\d\d)-\d\d/); if (monthmatch) { return monthmatch[1]; } else { monthmatch = this.getid(item.of[0] || '').toString().match(/\d\d\d\d-(\d\d)-\d\d/); if (monthmatch) { return monthmatch[1]; } return null; } }, date: (item) => { let itemvals = this.vals(item); if (itemvals?.length > 0) { let datematch = itemvals[0].toString().match(/\d\d\d\d-\d\d-\d\d/); if (datematch) { return datematch[0]; } else { datematch = this.getid(item.of[0] || '').toString().match(/\d\d\d\d-\d\d-\d\d/); if (datematch) { return datematch[0]; } else { if (this.dtype(itemvals[0], 'number')) { datematch = itemvals[0].toString().slice(0, 4); if (datematch) { return datematch; } } } } } return null; }, weekday: (item) => { const days = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']; if (item.date?.length > 0) return days[new Date(item.date).getDay()]; }, time: (item) => this.vals(item).map((v) => v.split('T')[1].slice(0, 5)), timeshift: (item) => { const vals = this.vals(item); let tsx = new Date(vals[0]); let adjust = Number(vals[1]) * 60 * 60 * 1000; tsx.setTime(tsx.getTime() + adjust); return tsx.toISOString(); }, hour: (item) => this.vals(item).map((v) => v.split('T')[1].split(':')[0]), datediff: (item) => { const [d1, d2] = this.vals(item); return ((d2 ? new Date(d2) : new Date()) - new Date(d1)) / (24 * 60 * 60 * 1000); }, timediff: (item) => { const [d1, d2] = this.vals(item); return (new Date(d2) - new Date(d1)); }, dateforms: (item) => { const basedate = this.vals(item)[0]; const [baseyear, basemonth, baseday] = basedate.split('-'); return [ basedate, `${basemonth}/${baseday}/${baseyear}`, basemonth.startsWith('0') ? `${basemonth.replace(/^0/, '')}/${baseday}/${baseyear}` : null, basemonth.startsWith('0') && baseday.startsWith('0') ? `${basemonth.replace(/^0/, '')}/${baseday.replace(/^0/, '')}/${baseyear}` : null, `${basemonth}/${baseday}/${baseyear.slice(2)}`, `${basemonth.replace(/^0/, '')}/${baseday.replace(/^0/, '')}/${baseyear.slice(2)}`, `${['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][Number(basemonth)]} ${baseday.replace(/^0/, '')}, ${baseyear}` ].filter(x => x); }, now: (item) => performance.now(), spandays: (item) => { const startdate = Array.isArray(item.start) ? item.start[0] : item.start; const enddate = Array.isArray(item.end) ? item.end[0] : item.end; const days = []; const d = new Date(startdate); d.setUTCHours(0, 0, 0, 0); const stop = new Date(enddate); stop.setUTCHours(0, 0, 0, 0); while (d < stop) { days.push(d.toISOString().slice(0, 10)); d.setUTCDate(d.getUTCDate() + 1); } return days; }, round: (item) => this.numvals(item).map((v) => Math.round(v)), roundaway: (item) => this.numvals(item).map((v) => Math.sign(v) * Math.round(Math.abs(v))), roundm: (item) => { const vals = this.numvals(item); const multiple = vals.pop(); return vals.map((v) => Math.round(v / multiple) * multiple); }, roundd: (item) => { const vals = this.numvals(item); const digits = vals.pop(); return vals.map((v) => { const factor = 10 ** (Math.floor(Math.log10(v)) - digits + 1); return Math.round(v / factor) * factor; }); }, floor: (item) => this.numvals(item).map((v) => Math.floor(v)), ceil: (item) => this.numvals(item).map((v) => Math.ceil(v)), n: (item) => { let i = 0; const test = this.getname(item.of[0]); for (const key in item) { if (key != 'of') { i--; if (key == test) return i; } } return i - 1; }, numbers: (item) => { const res = []; const firstnum = item.from != undefined && item.from != null ? Number(item.from) : 1; const lastnum = item.to != undefined && item.to != null ? Number(item.to) : Number(this.getname(item.of[0])); for (let x = firstnum; x <= lastnum; x++) { res.push(x) } return res; }, dehyphenate: (item) => this.vals(item).flatMap((v) => v.replaceAll(/(?<=\w)-\n(?=\w)/g, '')), deparen: (item) => this.vals(item).flatMap((v) => v.replaceAll(/\s*\(.*?\)\s*/g, '')), sentences: (item) => this.vals(item).flatMap((v) => v.split(/[.?!…]['"’”»]?\s+/)), allwords: (item) => this.vals(item).flatMap((v) => Array.from(v.toString().toLowerCase().matchAll(/[\p{Letter}\p{Number}]+(?:['‘’][\p{Letter}\p{Number}]+)?/gu).map((m) => m[0]))), words: (item) => this.vals(item).flatMap((v) => Array.from(v.toString().toLowerCase().matchAll(/[\p{Letter}\p{Number}]+(?:['‘’][\p{Letter}\p{Number}]+)?/gu).map((m) => m[0])).filter((w) => w.length >= 4)), characters: (item) => this.vals(item).flatMap((v) => Array.from(v.toString())), 'character count': (item) => this.vals(item).flatMap((v) => v.length), case: (item) => this.vals(item).map((v) => { const hasupper = v.match(/[A-Z]/); const haslower = v.match(/[a-z]/); if (!hasupper && !haslower) { return 'none'; } else if (hasupper && !haslower) { return 'upper'; } else if (haslower && !hasupper) { return 'lower'; } else { if (v[0].match(/[A-Z]/) && !(v.slice(1).match(/[A-Z]/))) { return 'initial'; } else { return 'mixed'; } } }), uppercase: (item) => this.vals(item).flatMap((v) => v.toString().toUpperCase()), lowercase: (item) => this.vals(item).flatMap((v) => v.toString().toLowerCase().replaceAll(/[‘’`]/g, "'").replaceAll(/[“”]/g, '"')), list: (item) => this.kkeys(item), items: (item) => item?.of?.flatMap((typename) => this.data?.[this.getname(typename)]), traverse: (item) => { const props = this.vals(item); return item?.of?.flatMap((subitem) => props.flatMap((prop) => this.step(subitem, prop, null, {}))); }, random: (item) => Math.random(), shuffle: (item) => { const newArray = [].concat(item?.of || []); for (let i = newArray.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [newArray[i], newArray[j]] = [newArray[j], newArray[i]]; } return newArray; }, 'weighted shuffle': (item) => { return item.of.map((subitem) => ({ weight: Number(subitem.weight || 1) * Math.random(), subitem: subitem })).sort((a, b) => b.weight - a.weight).map((ws) => ws.subitem); }, pick: (item) => item?.of?.[~~(Math.random() * item?.of?.length)], link: (item) => `${item.text}`, img: (item) => { if (item.uri) { return ``; } else { return ``; } }, sign: (item) => item.of.map((subitem) => { if (subitem.id) { return subitem; } else { const str = JSON.stringify(subitem); let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; } return Object.assign({id: (hash >>> 0).toString(36).padStart(7, '0')}, subitem); } }), 'parse query': (item) => item.of.map((subitem) => ({ name: subitem.name, query: subitem.query, assembly: this.parse(subitem.query) })), results: (item) => this.execute(item.of, this.parse(item.query || item.of?.[0])) } this.data.annotators = Object.entries(this.annotators).map(([key, code]) => ({id: key, code: code})); this.destinations = new Set(); } survey() { this.destinations = new Set(Object.keys(this.data).concat(this.data?.queries?.map((q) => q.name) || []).concat(Object.keys(this.adapters))); } vals(item) { const getname = this.getname; const itemvals = []; if (Object.keys(item).length > 1) { Object.entries(item).filter(([k, v]) => k != 'of').flatMap(([k, v]) => Array.isArray(v) ? v : [v]).map(getname).forEach((v) => itemvals.push(v)); } else { item.of.map(getname).forEach((v) => itemvals.push(v)); } return itemvals; } numvals(item) { return this.vals(item).filter((val) => this.dtype(val, 'number')).map((val) => Number(val)); } kkeys(item) { return Object.keys(item).filter((k) => k != 'of'); } async querylive(query, inputlist = null) { this.recache = new Set(); const res = await this.query(query, inputlist); return res; } async query(query, inputlist = null, loop = 50) { this.survey(); const operations = Array.isArray(query) ? query : this.assemble(this.tokenize(query)); const result = this.execute(inputlist, operations); this.data['current results'] = result; const queued = Object.keys(this.adapters).filter((key) => this.adapters[key].queue.length > 0); if (loop > 0 && queued.length > 0) { await this.adapt(); if (this.recache) queued.forEach((q) => this.recache.add(q)); const reres = await this.query(operations, inputlist, loop - 1); return reres; } else if (loop === 0) { this.recache = false; Object.keys(this.adapters).forEach((key) => { if (!this.adapters[key].annotator) { this.adapters[key].queue.forEach((id) => { (this.index[key] ||= {})[id] = null; }) } }) } this.recache = false; return result; } async adapt() { for (const key in this.adapters) { const adapter = this.adapters[key]; if (adapter.queue.length > 0) { if (adapter.annotator) { while (adapter.queue.length > 0) { this.statusf('annotating ' + key + ' ' + adapter.queue.length); const item_to_annotate = adapter.queue.shift(); try { const annotation = await adapter.f(item_to_annotate); (this.index[key] ||= {})[this.getid(item_to_annotate)] = annotation; this.indexlogit(key, 'write'); } catch (e) { console.error(e); } } } else { const newids = adapter.queue.filter((id) => !adapter.pending.has(id)); newids.forEach((id) => adapter.pending.add(id)); const res = await adapter.f(Array.from(new Set(newids))); newids.forEach((id) => adapter.pending.delete(id)); for (const item of res) { const id = this.getid(item); (this.index[key] ||= {})[id] = item; this.indexlogit(key, 'write'); if (key in this.data) { this.data[key].push(item); } } this.adapters[key].queue = []; } } } this.adaptive = false; } async rehope(only = null) { var reset = 0; Object.keys(this.index).forEach((k) => { if (!only || only == k) { const todelete = Object.keys(this.index[k]).filter((dk) => this.index[k][dk] == null || this.index[k][dk] == undefined); todelete.forEach((dk) => delete (this.index[k][dk])); reset += todelete.length; } }); return reset; } load(something, named, append = false) { if (!named) return if (!append || !(named in this.data)) this.data[named] = []; if (Array.isArray(something)) { something.forEach((somethingx) => this.data[named].push(somethingx)); } else if (typeof something == 'object') { const firstkey = Object.keys(something)[0]; const firstobj = something[firstkey]; if (typeof firstobj === 'object' && !Array.isArray(firstobj) && (firstkey in Object.values(firstobj) || 'id' in firstobj)) { this.data[named].push(...Object.values(something)); } else if (typeof firstobj === 'object' && !Array.isArray(firstobj)) { this.data[named].push(...Object.entries(firstobj).map(([key, val]) => ({id: key, ...val}))); } else if (typeof firstobj === 'string') { this.data[named].push(...Object.entries(something).map(([key, val]) => ({id: key, name: val}))); } else { this.data[named].push(...Object.entries(something).map(([key, val]) => ({id: key, value: val}))); } } else { this.data[named].push(something); } return this.data[named]; } async loadjsonl(something, named, append = false) { if (!named) return if (!append || !(named in this.data)) this.data[named] = []; if (typeof something == 'string' && (something.startsWith('http') || something.startsWith('file://'))) { const fetchres = await fetch(something); something = await fetchres.text(); } var rows = something.trim().split(/[\n\r]+/); for (const row of rows) { const rowdata = JSON.parse(row); if (rowdata) this.data[named].push(rowdata); } } async loadcsv(something, named, quoteChar = '"', delimiter = ',', headerrows = 1) { if (typeof something == 'string' && (something.startsWith('http') || something.startsWith('file://'))) { const fetchres = await fetch(something); something = await fetchres.text(); } var rows = something.split(/[\n\r]+/); const regex = new RegExp(`\\s*(${quoteChar})?(.*?)\\1\\s*(?:${delimiter}|$)`, 'gs'); const match = (line) => Array.from(line.matchAll(regex), (m) => m[2]); const headers = []; for (let hrowx = 0; hrowx < headerrows; hrowx++) { const hrow = rows.shift(); match(hrow).forEach((h, hx) => { if (hrowx === 0) { headers.push(h); } else { headers[hx] = headers[hx] + ' ' + h; } }); } const heads = headers.length > 0 ? headers : match(rows.shift()); var lines = rows.slice(0).filter((line) => line); const parsed = lines.map((line) => { return match(line).reduce((acc, cur, i) => { // replace blank matches with `null` const val = cur.length <= 0 ? null : (!isNaN(cur) ? Number(cur) : cur); const key = heads[i] ?? `{i}`; if (key == '') { return {...acc}; } else { return {...acc, [key]: val}; } }, {}); }); this.load(parsed, named); } apacheLogToDate(apacheTimestamp) { // Apache log format: [10/Oct/2000:13:55:36 -0700] // Remove brackets if present const cleanTimestamp = apacheTimestamp.replace(/^\[|\]$/g, ''); // Split into date/time and timezone parts const [dateTimePart, timezone] = cleanTimestamp.split(' '); // Parse the date/time part: dd/MMM/yyyy:HH:mm:ss const [datePart, hour, minute, second] = dateTimePart.split(':'); const [day, month, year] = datePart.split('/'); // Month mapping const months = { 'Jan': 0, 'Feb': 1, 'Mar': 2, 'Apr': 3, 'May': 4, 'Jun': 5, 'Jul': 6, 'Aug': 7, 'Sep': 8, 'Oct': 9, 'Nov': 10, 'Dec': 11 }; // Create Date object (months are 0-indexed in JS) const date = new Date( parseInt(year), months[month], parseInt(day), parseInt(hour), parseInt(minute), parseInt(second) ); // Handle timezone offset if present if (timezone) { const sign = timezone[0] === '+' ? 1 : -1; const tzHours = parseInt(timezone.slice(1, 3)); const tzMinutes = parseInt(timezone.slice(3, 5)); const offsetMs = sign * (tzHours * 60 + tzMinutes) * 60 * 1000; // Adjust for timezone (Apache logs are in local time, JS Date assumes UTC) date.setTime(date.getTime() - offsetMs); } return date; } async loadclf(loglines, named, apiroutes = []) { const lines = loglines.trim().split('\n'); const cols9 = ['ip', 'name', 'username', 'timestamp', 'requestraw', 'status', 'bytes', 'referrer', 'useragent']; const cols10 = ['host'].concat(cols9); const parsed = lines.filter((line) => line?.length > 0).map((line) => { const vals = Array.from(line.matchAll(/\"(?:\\"|.)*?\"|\[.*?\]|\S+/g)).map((m) => m[0]); const cols = vals.length == 10 ? cols10 : cols9; const obj = Object.fromEntries(vals.map((v, vx) => ([cols[vx], v]))); const date = this.apacheLogToDate(obj.timestamp); obj.timestamp = date.toISOString(); [obj.date, obj.time] = obj.timestamp.slice(0, -1).split('T'); if (obj.requestraw.match(/ [^ ]+ /) && !obj.requestraw.startsWith('"{')) { [obj.method, obj.request, obj.protocol] = obj.requestraw.slice(1, -1).split(' '); if (this.data['.clf API routes']) { for (const apiroute of this.data['.clf API routes'].sort((a, b) => b.length - a.length)) { if (obj.request.startsWith(apiroute)) { obj.page = apiroute; break; } } } obj.page ??= obj.request.split('?')[0]; } obj.referrer = obj.referrer.slice(1, -1); obj.useragent = obj.useragent.slice(1, -1); obj.logline = line; return obj; }); this.load(parsed, named); } async loadrss(rsstext, named) { const rssval = (k, rawval) => { if (k.match(/date/i)) { return new Date(rawval).toISOString(); } else if (!isNaN(rawval)) { return Number(rawval); } else { return rawval; } } const rssdom = new window.DOMParser().parseFromString(rsstext, "text/xml"); const items = Array.from(rssdom.querySelectorAll('item')).map((i) => { const obj = {}; Array.from(i.children).forEach((c) => { const k = c.tagName; const rawval = c.textContent.trim(); obj[k] = rssval(k, rawval); if (k.match(/date/i) && obj[k].length > 10 && !('date' in obj)) obj.date = obj[k].slice(0, 10) }); return obj; }); if (named) { this.load(items, named); } else { return items; } } async loadopml(opmltext, named) { const opmldom = new window.DOMParser().parseFromString(opmltext, "text/xml"); const feeds = Array.from(opmldom.querySelectorAll('outline[type="rss"]')).map((i) => ({ title: i.getAttribute('title'), feed: i.getAttribute('xmlUrl'), site: i.getAttribute('htmlUrl') })); if (named) { this.load(feeds, named); } else { return feeds; } } connect(key, adapter, doc = {}, annotator = null) { this.adapters[key] = {queue: [], pending: new Set(), f: adapter, annotator: annotator, doc: doc}; this.data.adapters ??= []; if (!this.data.adapters.find((a) => a.id == key)) this.data.adapters.push({ id: key, requires: doc.requires, produces: doc.produces, code: this.adapters[key].f }); if (annotator) this.register(key, (item) => this.resolve(key, item), adapter); } connect_annotator(key, adapter, required = [], doc = {}) { this.connect(key, adapter, doc, required); } register(key, annotator, displayf = null) { this.annotators[key] = annotator; this.data.annotators ??= []; if (!this.data.annotators.find((a) => a.id == key)) this.data.annotators.push({ id: key, code: displayf || this.annotators[key] }); } unbracket(token) { if (!(typeof token == 'string')) token = token.toString(); if (token.startsWith('[') && token.endsWith(']')) { return token.slice(1, -1).replaceAll(']]', ']'); } return token; } bracket(token) { if (!(typeof token == 'string')) token = token?.toString() ?? ''; if (token.match(/[?.:#\/|!<>=~@\[\]\(\),;\+-]/) || token.startsWith(' ') || token.endsWith(' ')) { return '[' + token.replaceAll(']', ']]') + ']'; } return token; } escapeRegExp(string) { return string.replace(/[.*+?${}()|[\]\\]/g, "\\$&"); } tokenize(text) { if (typeof text != 'string') text = String(text); const matches = text.matchAll(/(\[(?:\]\]|[^\]])*\])|(\?{1,3})|(\.{1,4})|(\/{1,2})|([\:\#\|\!])|(\()|(\))|([@~=<>\+-]+)|(\,)|(\;)|([^\[\]\(\)\.\?\:\/\#\|\!~=<>@,;\+-]+)|([\[\]])/gms); const tokenlist = Array.from(matches, m => m[0].trim()).filter((token) => token.length > 0); return tokenlist; } assemble(tokenized) { const isOperator = (token) => '???....://#|!'.includes(token); const tokenlist = '= ~ =~ ~< ~> > < <> >< >= <= - =- ~- + =+ @ @@ @- @= @@= =@ =@@ @< @@< @<= @@<= @> @@> @>= @@>= =>'.split(' '); const isSubop = (token) => tokenlist.includes(token) || (token?.startsWith('-') && (token.length == 1 || tokenlist.includes(token.slice(1)))); const isSeparator = (token) => ',;'.includes(token); const isValue = (token) => !isOperator(token) && !isSubop(token) && !isSeparator(token); let tokens = tokenized.slice(); let level = 0; const operations = []; while (tokens.length > 0) { const op = {operator: null, args: []}; let token = tokens.shift(); if (isOperator(token) || ((isValue(token) || isSubop(token)) && operations.length === 0)) { if ((isValue(token) || isSubop(token)) && operations.length === 0) { op.operator = '?'; tokens.unshift(token); } else { op.operator = token; } while (tokens.length > 0 && !isOperator(tokens[0])) { const arg = {separator: null, label: null, subop: null, value: null}; if (isSeparator(tokens[0])) { arg.separator = tokens.shift(); } while (tokens.length > 0 && !isOperator(tokens[0]) && !isSeparator(tokens[0])) { const frag = tokens.shift(); if (frag != '(' && isValue(frag) && isSubop(tokens[0])) { arg.label = this.unbracket(frag); arg.subop = tokens.shift(); } else if (isSubop(frag)) { arg.subop = frag; } else if (frag === '(') { const subquery = []; level++; while (tokens.length > 0 && level > 0) { const sub = tokens.shift(); if (sub === '(') { level++; if (level > 0) { subquery.push(sub); } } else if (sub === ')') { level--; if (level > 0) { subquery.push(sub); } } else if (level > 0) { subquery.push(sub); } } const subexpr = this.assemble(subquery); arg.value = subexpr; } else if (frag.match(/^[=<>@~-]+$/)) { const failure = { tokens: tokenized, assembled: operations.slice(0), assembling: {op: op, arg: arg, unexpected: frag}, unassembled: tokens.slice(0) } throw new Error("Invalid subop", {cause: failure}); } else if (!arg.value) { arg.value = this.unbracket(frag); } else { const failure = { tokens: tokenized, assembled: operations.slice(0), assembling: {op: op, arg: arg, unexpected: frag}, unassembled: tokens.slice(0) } throw new Error("Unexpected token", {cause: failure}); } } op.args.push(arg); } } operations.push(op); } if (level > 0) console.warn({parentropy: level, operations: operations}); return operations; } parse(querystr) { return this.assemble(this.tokenize(querystr)); } disassemble(assembly) { return assembly.map((operation, ox) => operation.operator + ((operation.args.length == 0 && ox < assembly.length - 1 && operation.operator[0] == assembly[ox + 1].operator[0]) ? ' ' : operation.args.map((arg) => (arg.separator ?? '') + (arg.label ? this.bracket(arg.label) : '') + (arg.subop ?? '') + (Array.isArray(arg.value) ? ('(' + this.disassemble(arg.value) + ')') : (arg.value ? this.bracket(arg.value) : ''))).join(''))).join('').replace(/^\?(?=[a-z])/, ''); } compact(querystr) { return this.disassemble(this.parse(querystr)); } executeq(querystr) { return this.execute([], this.assemble(this.tokenize(querystr))); } timecheck(timer, i, count, op) { if (i == 1) timer.loopstart = new Date(); if (i >= 10 && i >= count / 100) { const taken = new Date() - timer.loopstart; const projected = count * taken / i; if (this.timelimit && projected > this.timelimit) throw new Error('Query overrun.', { cause: { operation: this.disassemble([op]), items: count, done: i, elapsed: taken + 'ms', projected: Math.round(projected / 60000) + ' minutes', timelimit: Math.round(this.timelimit / 60000) + ' minutes' } }); } } unmmss = (mmssstr) => { const parts = mmssstr.split(':'); let s = 0; const seconds = parts.pop(); if (seconds) s += Number(seconds); const minutes = parts.pop(); if (minutes) s += 60 * Number(minutes); const hours = parts.pop(); if (hours) s += 60 * 60 * Number(hours); return s; } dethe = (value) => { return value.toLowerCase().replace(/^the /, '') }; compvals = (araw, braw) => { const a = araw.toString().trim(); const b = braw.toString().trim(); const atime = a.match(/^(?:\d+)(?:\:\d{2,})+$/); const btime = b.match(/^(?:\d+)(?:\:\d{2,})+$/); if (atime && btime) { return this.unmmss(btime[0]) - this.unmmss(atime[0]); } return this.dethe(a).localeCompare(this.dethe(b)); } execute(inputlistraw, operations, labeled = null, level = null) { let inputlist = Array.isArray(inputlistraw) ? inputlistraw : inputlistraw ? [inputlistraw] : []; let currentlist = inputlist?.slice(0) || []; const getname = this.getname; const getid = this.getid; const dtype = this.dtype; const nullish = this.nullish; const dcopy = this.dcopy; const opclone = this.opclone; const step = this.step; const dethe = this.dethe; const compvals = this.compvals; labeled ||= {}; let outputlist = []; const toplevel = !inputlistraw && !level; if (toplevel && operations[0]?.progress) { currentlist = operations[0].progress; labeled = operations[0].labeled; outputlist = currentlist; } for (let opx = 0; opx < operations.length; opx++) { const op = operations[opx]; if (op.completed) continue; const opstart = performance.now(); outputlist = []; switch (op.operator) { case '?': // start if (op.args.length == 0) { outputlist = Object.keys(this.data).filter((x) => !this.internal_datasets.includes(x)).sort((a, b) => dethe(a).localeCompare(dethe(b))); break; } outputlist = []; op.args.forEach((arg) => { let startitems = []; if (arg.subop?.includes('+')) { if (arg.label) { if (arg.label in labeled) { labeled[arg.label].forEach((i) => outputlist.push(i)); } else if (arg.label in this.data) { this.data[arg.label].forEach((i) => outputlist.push(i)); } } else { currentlist.forEach((i) => outputlist.push(i)); } } let typeitems; if (arg.separator != ';' || outputlist.length == 0) { if (Array.isArray(arg.value)) { startitems = this.execute(arg.subop?.includes('+') ? outputlist : [], arg.value, labeled); } else if (arg.subop == '~') { startitems = [arg.value]; } else if (arg.value in labeled) { startitems = labeled[arg.value]; } else if (typeitems = this.gettype(arg.value)) { startitems = typeitems; } else if (arg.subop != '=') { if (!isNaN(arg.value)) { startitems = [Number(arg.value)]; } else { startitems = [arg.value]; } } if (arg.label) this.index[arg.label] = {}; if (arg.subop?.includes('+')) { if (arg.label) { startitems.forEach((i) => { if (arg.label in labeled) (labeled[arg.label] ||= []).push(i); outputlist.push(i); }); } else { startitems.forEach((i) => outputlist.push(i)); } } else if (arg.subop?.includes('-')) { const startids = new Set(startitems.map((si) => getid(si))); if (arg.label && labeled[arg.label]) { labeled[arg.label] = labeled[arg.label].filter((li) => !startids.has(getid(li))); outputlist = labeled[arg.label]; } else { outputlist = outputlist.filter((li) => !startids.has(getid(li))); } } else { if (arg.label) labeled[arg.label] = startitems; startitems.forEach((si) => outputlist.push(si)); } } }); break; case '??': // label outputlist = currentlist; let ended = false; op.args.forEach((arg) => { if (!ended) { if (arg.label == '_timelimit' && !isNaN(arg.value)) { this.timelimit = Number(arg.value) * 60000; } else if (arg.label == 'status') { if (arg.value) { this.statusf(arg.value); } else { this.statusf(currentlist[0]); } } else if (arg.label) { if (arg.subop.includes('~') && dtype(arg.value, 'string')) { labeled[arg.label] = [arg.value]; // } else if (arg.labeled) { // labeled[arg.label] = arg.labeled; } else { if (arg.subop?.match(/\+/)) labeled[arg.label] ||= []; const newvals = this.execute(currentlist, Array.isArray(arg.value) ? arg.value : '.' + arg.value, labeled, level); if (arg.subop?.match(/\+/)) { newvals.forEach((nv) => labeled[arg.label].push(nv)); } else if (arg.subop?.includes('-') && labeled[arg.label]) { const newids = new Set(newvals.map((ni) => getid(ni))); labeled[arg.label] = labeled[arg.label].filter((li) => !newids.has(getid(li))); } else { labeled[arg.label] = newvals; } // if (!level && opx == 0) arg.labeled = newvals; } this.index[arg.label] = {}; } else if (dtype(arg.value, 'string')) { if (arg.subop?.match(/\+/)) { labeled[arg.value] ??= []; const currentids = new Set(currentlist.map((ci) => getid(ci))); currentlist.forEach((i) => labeled[arg.value].push(i)); } else if (arg.subop?.includes('-') && labeled[arg.value]) { const currentids = new Set(currentlist.map((ci) => getid(ci))); labeled[arg.value] = labeled[arg.value].filter((li) => !currentids.has(getid(li))); } else { labeled[arg.value] = currentlist.slice(0); } this.index[arg.value] = {}; if (arg.value == 'end') { ended = true; } } } }); if (ended) return (this.debug && !inputlist) ? operations : outputlist; break; case '!': // repeat if (opx > 0) { const repeat_ops = operations.slice(opx - 1, opx + 1); repeat_ops.forEach((rop) => { delete (rop.progress); delete (rop.labeled); delete (rop.completed); }); outputlist = currentlist.slice(0); level ??= 0; level += 1; const maxrecursion = ((op.args.length > 0 && op.args[0].value) || 1000); if (outputlist.length > 0 && level < maxrecursion && (!this.samearray(inputlist, outputlist) || level == 1)) { const recursed = this.execute(outputlist, repeat_ops, labeled, level); if (recursed.length > 0 && !this.samearray(recursed, outputlist)) outputlist = recursed.slice(0); } } break; case '.': // traverse case '..': // traverse with duplicates const seen = new Set(); const sofar = Array.isArray(op.args?.[0]?.value) && op.args?.[0]?.label; if (sofar) labeled[sofar] = []; const traversetimer = {}; outputlist = currentlist.reduce((acc, item, i) => { this.timecheck(traversetimer, i, currentlist.length, op); if (op.args.length == 0) { const itemkey = getid(item); if (op.operator == '..' || !seen.has(itemkey)) { acc.push(item); seen.add(itemkey); } return acc; } if (op.args[0].subop?.includes('<') && acc.length > 0) return acc; let itemvals = []; let toremoveids = {}; for (const arg of op.args.filter((arg) => arg.value != null && arg.value != undefined)) { if (arg.separator != ';' || itemvals.length == 0) { let itemval; if (arg.subop?.includes('@') && arg.label != null && !isNaN(arg.value)) { const itemvalraw = step(item, arg.label, null, labeled); if (arg.subop.includes('@@')) { itemval = itemvalraw.slice(itemvalraw.length - Number(arg.value)); } else { itemval = itemvalraw.slice(0, Number(arg.value)); } } else if (arg.subop?.includes('=') && (arg.subop?.includes('>') || arg.label == null)) { if (arg.value in item) { itemval = item[arg.value]; } } else { itemval = step(item, arg.value, arg.subop, labeled); } if (itemval) { if (!Array.isArray(itemval)) itemval = [itemval]; if (arg.subop?.includes('-') && isNaN(arg.value)) { itemval.forEach((subitem) => { const subid = getid(subitem); toremoveids[subid] ||= 0; toremoveids[subid] += 1; }); } else { var passdown = null; if (arg.subop?.includes('>')) { if (arg?.label in item) { passdown = item[arg.label]; } else { passdown = JSON.parse(JSON.stringify(item, Object.keys(item).filter((k) => k != arg.value))); } } itemval.forEach((subitem) => { if (passdown) { if (!dtype(subitem, 'object')) subitem = {value: subitem}; if (arg.label) { subitem[arg.label] = Array.isArray(passdown) ? passdown : [passdown]; } else { Object.assign(subitem, passdown); } } const subitemkey = getid(subitem); if (op.operator == '..' || !seen.has(subitemkey)) { seen.add(subitemkey); itemvals.push(subitem); if (sofar) labeled[sofar].push(subitem); } }) } } } if (Object.keys(toremoveids).length > 0) { if (op.operator == '.') { itemvals = itemvals.filter((subitem) => !(getid(subitem) in toremoveids)); } else { itemvals = itemvals.filter((subitem) => { const subid = getid(subitem); if (toremoveids[subid] > 0) { toremoveids[subid] -= 1; return false; } else { return true; } }) } } } itemvals.forEach((x) => acc.push(x)); return acc; }, []); outputlist = outputlist.filter((item) => item != null); break; case ':': // filter if (!(op?.args?.length > 0)) { outputlist = currentlist; break; } if (op?.args?.length == 1 && op.args[0].subop && op.args[0].label == null && op.args[0].value == null) { let checklist; if (currentlist.length == 0) { outputlist = []; break; } else if (currentlist.length == 1) { checklist = [currentlist[0], currentlist[0]]; } else { checklist = currentlist; } let comparator = op.args[0].subop; let polarize = (x) => x; let check; if (comparator?.startsWith('-') || comparator?.endsWith('-')) { polarize = (x) => !x; comparator = comparator.replace(/-|-$/, ''); } for (let pi = 0; pi < checklist.length - 1; pi++) { let a = getname(checklist[pi]); let b = getname(checklist[pi + 1]); switch (comparator) { case '': check = polarize(a == b); break; case '=': check = polarize(a == b); break; case '>=': check = polarize(a >= b); break; case '<=': check = polarize(a <= b); break; case '>': check = polarize(a > b); break; case '<': check = polarize(a < b); break; case '~': check = polarize(a?.toString().toLowerCase().includes(b?.toString().toLowerCase())); break; case '~<': check = polarize(a?.toString().toLowerCase().startsWith(b?.toString().toLowerCase())); break; case '~>': check = polarize(a?.toString().toLowerCase().endsWith(b?.toString().toLowerCase())); break; } if (!check) { outputlist = []; break; } } if (check) outputlist = currentlist; break; } for (const arg of op.args) { if ([null, '', '=', '~'].includes((arg.subop || '').replace(/-/, '')) && dtype(arg.value, 'string') && arg.value.startsWith('~')) { const flags = arg.value.startsWith('~~') ? 'i' : ''; const pattern = arg.value.replace(/^~*/, ''); const fullpattern = (arg.subop || '').replace(/-/, '') == '=' ? ((pattern.startsWith('^') ? '' : '^') + pattern + (pattern.endsWith('$') ? '' : '$')) : pattern; arg.re = new RegExp(fullpattern, flags); } } const ands = [[]]; for (const arg of op.args) { if (arg.separator == ';') { ands.push([arg]); } else { ands[ands.length - 1].push(arg) } } const filtertimer = {}; outputlist = currentlist.filter((item, i) => { this.timecheck(filtertimer, i, currentlist.length, op); return ands.filter((and) => { return and.filter((arg) => { let comparator = arg.subop; let polarize = (x) => x; if (comparator?.startsWith('-') || comparator?.endsWith('-')) { polarize = (x) => !x; comparator = comparator.replace(/-|-$/, ''); } if (!comparator && arg.label == null && Array.isArray(arg.value)) { return polarize(this.execute([item], arg.value, labeled).length > 0); } else if (['+', ''].includes(comparator) && arg.label && !arg.value && dtype(arg.label, 'string') && dtype(item, 'object')) { const propval = item[arg.label]; // console.log({plusminus: comparator, label: arg.label, propval: propval, judgment: polarize(Array.isArray(propval) ? propval.length > 0 : propval)}) return polarize(Array.isArray(propval) ? propval.length > 0 : propval); } let testitems = [item]; if (arg.label && dtype(item, 'object')) { testitems = step(item, arg.label, null, labeled); } const testvals = testitems.map((testitem) => { if (comparator || arg.re) { return getname(testitem); } else if (this.dtype(testitem, 'literal')) { return testitem; } else if ('id' in testitem) { return testitem.id; } else { return getname(testitem) } }); let argvals; if (Array.isArray(arg.value) || arg.value?.startsWith('=')) { const argvalitems = this.step(item, arg.value, null, labeled); argvals = argvalitems.map((argvalitem) => { let argval; if (dtype(argvalitem, 'literal')) { argval = argvalitem; } else { argval = getname(argvalitem); } if (argval == null) { const argvalitemkeys = Object.keys(argvalitem).filter((key) => key != 'id'); if (argvalitemkeys.length == 1) { argval = argvalitem[argvalitemkeys[0]]; } } return argval; }); } return testvals.find((testval) => { if (!argvals) { if (dtype(testval, 'number') && dtype(arg.value, 'number')) { testval = Number(testval); argvals = [Number(arg.value)]; } else { if (isNaN(testval) || !isFinite(testval)) testval = testval.toString(); argvals = [arg.value]; } } if (comparator?.startsWith('@')) { if (dtype(arg.value, 'number')) { argvals = [Number(arg.value)]; } else if (arg.value in labeled) { argvals = Number(labeled[arg.value]); if (!Array.isArray(argvals)) argvals = [argvals]; } if (comparator.startsWith('@@')) { testval = currentlist.length - i; comparator = comparator.slice(2); } else { testval = i + 1; comparator = comparator.slice(1); } } if (testval == null || argvals == null || argvals.length == 0) return polarize(false); return argvals.find((argval) => { if (arg.re) { return polarize(testval.match(arg.re)); } else { switch (comparator) { case null: return polarize(testval == argval); case '': return polarize(testval == argval); case '=': return polarize(testval == argval); case '>=': return polarize(testval >= argval); case '<=': return polarize(testval <= argval); case '>': return polarize(testval > argval); case '<': return polarize(testval < argval); case '~': return polarize(testval?.toString().toLowerCase().includes(argval?.toString().toLowerCase())); case '~<': return polarize(testval?.toString().toLowerCase().startsWith(argval?.toString().toLowerCase())); case '~>': return polarize(testval?.toString().toLowerCase().endsWith(argval?.toString().toLowerCase())); } } }) != null; }) != null; }).length > 0; }).length == ands.length; }); break; case '#': // sort const sortargs = op.args.slice(0); const lastarg = sortargs[sortargs.length - 1]; let temped = false; if (!lastarg || ![';', '=;'].includes(Object.values(lastarg).join(''))) sortargs.push(...[{ subop: null, value: 'name' }, {subop: null, value: 'id'}]); if (!dtype(currentlist[0], 'object') && lastarg && lastarg.separator == ';') { currentlist = currentlist.map((v) => ({_value: v})); temped = true; } else if (sortargs[0]?.label) { currentlist = currentlist.map((i) => dcopy(i)); } sortargs.forEach((arg) => { arg.extraindex = {}; const stablesort = (arg.label == null && arg.value == null && arg.separator == ';') ? (arg.subop == '-' ? -1 : 1) : null; if (arg.subop?.includes('~')) { arg.sortmode = 'literal'; } else if (arg.subop?.endsWith('-') || arg.subop?.endsWith('>')) { arg.sortmode = 'numeric'; } else if (arg.subop?.endsWith('+') || arg.subop?.endsWith('<')) { arg.sortmode = 'rank'; } else { arg.sortmode = stablesort || ['rank', 'index', 'number', 'id'].includes(arg.value) ? 'rank' : 'numeric'; } const vals = new Set(); const extradone = new Set(); for (let ix = 0; ix < currentlist.length; ix++) { const item = currentlist[ix]; const vallist = stablesort ? [ix] : (arg.value ? step(item, arg.value, null, labeled) : (Array.isArray(item) ? item : [item])); if (vallist && !stablesort) vallist.forEach((val) => vals.add(val)); if (typeof item == 'object') { (item._sortindex ||= []).push(vallist); } else if (!dtype(item, 'number') && !extradone.has(item)) { arg.extraindex[item] = vallist; extradone.add(item); } } ; if (arg.sortmode != 'literal' && !arg.subop?.endsWith('>') && !arg.subop?.endsWith('<')) { for (const val of vals) { if (val != null && !dtype(val, 'number') && !(dtype(val, 'object') && dtype(getname(val), 'number'))) { arg.sortmode = null; break; } } ; } }) outputlist = currentlist.sort((a, b) => { let comp = 0; let ii = 0; for (const arg of sortargs) { const alist = typeof a == 'object' ? a._sortindex[ii] : dtype(a, 'number') ? [Number(a)] : arg.extraindex[a]; const blist = typeof b == 'object' ? b._sortindex[ii] : dtype(b, 'number') ? [Number(b)] : arg.extraindex[b]; ii++; if (alist && blist) { if (arg?.subop?.includes('@')) { const ifunc = arg.subop == '@=' ? getname : getid; const alookup = alist.indexOf(ifunc(a)); const blookup = blist.indexOf(ifunc(b)); if (alookup > -1 && blookup == -1) { comp = -1; } else if (alookup == -1 && blookup > -1) { comp = 1; } else { const aliststrs = alist.map((ax) => ax.toString()); comp = aliststrs.indexOf(ifunc(a).toString()) - aliststrs.indexOf(ifunc(b).toString()); } } else { for (let i = 0; i < Math.min(alist.length, blist.length); i++) { let aitem = alist[i]; let bitem = blist[i]; const anull = nullish(aitem); const bnull = nullish(bitem); if (anull && bnull) { comp = 0; } else if (bnull) { comp = -1; } else if (anull && !bnull) { comp = 1; } else { switch (arg.sortmode) { case 'literal': const atrim = typeof aitem == 'string' ? aitem.trim() : aitem; const btrim = typeof bitem == 'string' ? bitem.trim() : bitem; comp = atrim < btrim ? -1 : (btrim < atrim ? 1 : 0); break; case 'numeric': case 'rank': const polarity = (arg.sortmode == 'numeric' ? -1 : 1); let bnum = Number(bitem); let anum = Number(aitem); if (isNaN(bnum) || isNaN(anum)) { bnum = Number(getname(bitem)); anum = Number(getname(aitem)); } if (isNaN(bnum) && isNaN(anum)) { comp = polarity * compvals(aitem, bitem); } else if (isNaN(anum)) { comp = 1; } else if (isNaN(bnum)) { comp = -1; } else { comp = polarity * (anum - bnum); } break; default: if (dtype(aitem, 'literal') && dtype(bitem, 'literal')) { comp = compvals(aitem, bitem); } else if (typeof aitem == 'object' && typeof bitem == 'object') { let aval = getname(aitem); let bval = getname(bitem); if (!nullish(aval) && !nullish(bval)) { comp = compvals(aval, bval); } else { aval = getid(aitem); bval = getid(bitem); if (!nullish(aval) && !nullish(bval)) { comp = compvals(aval, bval); } else { comp = 0; } } } break; } } if (comp != 0) break; } } } if (comp === 0) { if (alist && blist) { comp = blist.length - alist.length; } else if (alist) { comp = -1; } else if (blist) { comp = 1; } } if (arg?.subop?.includes('-') && (arg.sortmode == 'literal' || !arg.sortmode)) { comp = -comp; } if (comp != 0) break; } return comp; }) if (temped) outputlist = outputlist.map((vt) => vt._value); outputlist.forEach((item, i) => { delete item._sortindex; if (op.args.length > 0 && op.args[0].label) item[op.args[0].label] = i + 1; }); break; case '/': // group case '//': // merge const groupindex = new Map(); let keylists = {}; let ofname = 'of'; let countname = 'count'; let sortgroups = true; const groupargs = []; const accumulates = {}; const discards = new Set(); const merge_ands = []; for (const arg of op.args) { if (op.operator == '//' && arg.subop?.includes('+') && typeof arg.value == 'string') { accumulates[arg.value] = arg.label || arg.value; } else if (op.operator == '//' && arg.subop?.includes('-') && typeof arg.value == 'string') { discards.add(arg.value); } else if (arg.separator == ';' && arg.label == null && arg.value == null) { sortgroups = false; } else if (op.operator == '//' && (arg.separator == ';' || merge_ands.length > 0)) { if (arg.separator == ';') { merge_ands.push([arg.value]); } else { merge_ands[merge_ands.length - 1].push(arg.value) } } else { groupargs.push(arg); } } if (groupargs.length == 0) groupargs.push({value: null}); groupargs.forEach((arg) => arg.groupcounter = 0); const grouptimer = {}; for (let ix = 0; ix < currentlist.length; ix++) { this.timecheck(grouptimer, ix, currentlist.length, op); const item = currentlist[ix]; let keys = null; let keyi = 0; let itemsleft = currentlist.length - ix; for (const arg of groupargs) { if (arg.label == 'of') { ofname = arg.value; continue; } else if (arg.label == 'count') { countname = arg.value; continue; } keyi++; let groupnumber = null; if (op.operator == '/' && dtype(arg.value, 'number')) { if (arg.subop?.endsWith('@')) { arg.divisor = Number(arg.value); } else { arg.divisor = currentlist.length / Number(arg.value); } groupnumber = Math.floor(ix / arg.divisor) + 1; } else if (arg.value != null && arg.subop?.endsWith('@@')) { const groupval = step(item, arg.value, null, labeled); if (ix == 0 || groupval?.length > 0) arg.groupcounter += 1; groupnumber = arg.groupcounter; } const label = arg.label ?? (typeof arg.value === 'string' ? arg.value : null) ?? keyi; const newkeyitems = groupnumber != null ? [groupnumber] : arg.value ? step(item, arg.value, arg.subop, labeled) : [null]; const newkeys = newkeyitems.map((newkey) => ([{ arglabel: arg.label, label: label, keyitem: this.resolve(arg.value, newkey, labeled) }])); if (arg.subop?.includes('~') && Array.isArray(arg.value)) newkeys.forEach((newkey) => newkey[0].keyitem = getname(newkey[0].keyitem)); if (keys) { keys = keys.flatMap((oldkeys) => newkeys.filter((newkey) => arg.separator == ',' || oldkeys.filter((oldkey) => compvals(getname(oldkey.keyitem) || oldkey.keyitem, getname(newkey[0].keyitem) || newkey[0].keyitem) >= 0).length === 0).map((newkey) => oldkeys.concat(newkey))); } else { keys = newkeys; } if (arg.subop?.endsWith('@') || dtype(arg.value, 'number')) { keys.forEach((key) => { const testkey = JSON.stringify(key.slice(0, -1)); const newkeytest = JSON.stringify(key[key.length - 1]); if (testkey in keylists) { const lastkey = keylists[testkey][keylists[testkey].length - 1]; if (newkeytest != lastkey) keylists[testkey].push(newkeytest); } else { keylists[testkey] = [newkeytest]; } key[key.length - 1].keyindex = keylists[testkey].length; }); } } if (keys) { for (const key of keys) { const keystr = JSON.stringify(key); if (!groupindex.has(keystr)) groupindex.set(keystr, []); groupindex.get(keystr).push(item); } } } for (const [keystr, items] of groupindex) { if (op.operator == '/') { const newgroup = {}; const keydata = JSON.parse(keystr); if (keydata.length == 1 && keydata[0].keyitem != null) { if (keydata[0].keyindex) { newgroup.keyindex = keydata[0].keyindex; } else { let keyitemname = getname(keydata[0].keyitem); if (keyitemname != null) { newgroup.name = keyitemname; } } } let skip = false; let keys = []; for (const {arglabel, label, keyitem, keyindex} of keydata) { if (keyitem != null) { const keyobj = (dtype(keyitem, 'object') || keyindex == null || keyindex == undefined) ? keyitem : (keyindex != null && keyindex != undefined) ? {} : {name: keyitem}; if (keyindex != null) { keyobj.keyindex = keyindex; } keys.push(keyobj) if (label && dtype(label, 'string') && isNaN(label) && label != '_') { newgroup[label] = [keyobj]; } } } newgroup[countname] = items.length; if (keys) newgroup.key = keys; newgroup[ofname] = items; outputlist.push(newgroup); } else if (op.operator == '//') { const groupprops = groupargs.map((grouparg) => grouparg.value).filter((gp) => gp); if (merge_ands.length == 0 || !merge_ands.find((mand) => !mand.find((mr) => items.find((item) => mr in item)))) { const newgroup = items.reduce((acc, item) => { const proporder = groupprops.slice(0); Object.keys(item).forEach((prop) => { if (!proporder.includes(prop) && !discards.has(prop)) proporder.push(prop); }); for (const prop of proporder) { const writeprop = accumulates[prop] || prop; if (writeprop in acc) { if (Array.isArray(acc[writeprop])) { (Array.isArray(item[prop]) ? item[prop] : [item[prop]]).filter((val) => !acc[writeprop].includes(val)).forEach((val) => acc[writeprop].push(val)); } } else { if (prop in accumulates && !Array.isArray(item[prop])) { acc[writeprop] = [item[prop]]; } else { acc[writeprop] = item[prop]; } } } return acc; }, {}); const keydata = JSON.parse(keystr); for (const {arglabel, label, keyitem, keyindex} of keydata) { if (arglabel && typeof arglabel === 'string' && arglabel != '_') { newgroup[arglabel] = [keyitem]; } } outputlist.push(newgroup); } } } if (sortgroups) { const sortquery = '#' + groupargs.map((arg, argx) => (arg.subop?.endsWith('@') || arg.divisor ? '+' : '') + '(..key:@' + (argx + 1) + (arg.subop?.endsWith('@') ? '.keyindex;_' : '') + ')').join(','); outputlist = this.execute(outputlist, this.assemble(this.tokenize(sortquery)), labeled); } break; case '...': // synthesize case '....': // synthesize and extract if (!(op?.args?.length > 0)) { if (op.operator == '...') { outputlist = [{of: currentlist.map((item) => dcopy(item))}]; } else { outputlist = [currentlist.length] } break; } const segments = [[]]; op.args.forEach((arg) => { if (arg.separator == ';') segments.push([]); segments[segments.length - 1].push(arg); }); outputlist = []; let labels = null; let masterof = op.operator == '...'; if ( segments.length > 1 && segments[0].filter((s) => s.subop && s.value == null).length == segments[0].length && segments.slice(1).filter((s) => s.length == segments[0].length || (segments[0][segments[0].length - 1].label == null && s.length == segments[0].length - 1)).length == segments.length - 1 ) { labels = segments.shift(); const lastlabel = labels[labels.length - 1]; if (lastlabel.subop == '~' && lastlabel.label == null) { masterof = false; labels.pop(); } } for (const segment of segments) { const tempitem = {of: currentlist.map((item) => dcopy(item))}; const finalitem = {}; const postitem = {}; let aggregated = null; let afteraggregated = 0; let finalvalue = null; let includeof = masterof; segment.forEach((arg, propx) => { if (labels?.[propx]) { arg.label = labels[propx].label; arg.subop = labels[propx].subop; } if (arg.subop == '~' && arg.label == null && arg.value == null) { includeof = false } else if (!aggregated && arg.subop?.includes('~') && dtype(arg.value, 'string')) { tempitem[arg.label || '_' + (propx + 1).toString()] = arg.value; } else if (!aggregated && arg.subop?.includes('~') && Array.isArray(arg.value)) { const subresult = this.execute(currentlist, arg.value.map(opclone), labeled); tempitem[arg.label || '_' + (propx + 1).toString()] = subresult.length > 0 ? getname(subresult[0]) : null; } else { const prop = arg.label || (typeof arg.value === 'string' ? arg.value : null) || '_' + (propx + 1).toString(); const aggname = (arg.label == null && typeof arg.value == 'string' && arg.value) || (arg.label && arg.subop == null && arg.value == null); const firstitem = currentlist[0]; const isprop = typeof firstitem == 'object' && prop in firstitem; if ((arg.subop == '=' || !isprop) && arg.subop != '~' && aggname in this.annotators) { const aggval = this.annotators[aggname](tempitem); const agglabel = arg.label || aggname; if (aggval != null) finalitem[agglabel] = aggval; aggregated = agglabel; finalvalue = dcopy(aggval); } else if (typeof arg.value == 'string' && arg.value.startsWith('~~')) { let template = arg.value.slice(2); const extractionfield = arg.label || template; const targetfields = this.kkeys(tempitem); const fieldmap = {}; template = this.escapeRegExp(template); let tx = 1; targetfields.forEach((tf) => { const fieldwidth = this.getnumber(tempitem[tf]); const innerpattern = fieldwidth == 1 ? '.' : fieldwidth ? `.{${fieldwidth}}` : '.+?'; if (tf == '_') { template = template.replaceAll('_', `(?:${innerpattern})`); } else { const tkey = 'x' + tx; fieldmap[tf] = tkey; template = template.replace(this.escapeRegExp(tf), `(?<${tkey}>${innerpattern})`); tx += 1; } }); template = `^${template}$`; const extractor = new RegExp(template); finalitem[extractionfield] = []; finalitem['_template'] = template; finalitem['_field map'] = Object.entries(fieldmap).map(([k, v]) => ({ field: k, code: v })); tempitem.of.forEach((i) => { const extraction = getname(i).match(extractor); if (extraction) { const extractitem = {}; targetfields.forEach((k) => { extractitem[k.trim()] = extraction.groups[fieldmap[k]]; }); finalitem[extractionfield].push(extractitem); } }); finalvalue = finalitem[extractionfield]; aggregated = template; } else if (!aggregated) { let propitems; if (Array.isArray(arg.value) || !arg.label || arg.value?.startsWith('~')) { const propres = this.execute(tempitem.of, Array.isArray(arg.value) ? arg.value.map(opclone) : [{ operator: '..', args: [{value: arg.value}] }], labeled); if (arg.subop?.includes('~') && propres.length > 0) { propitems = getname(propres[0]); } else { propitems = propres; } } else if (arg.value?.startsWith('=')) { const mathres = this.step(tempitem.of, arg.value, arg.subop, labeled); if (mathres.length > 0) propitems = mathres[0]; } else { if (arg.subop?.includes('~')) { propitems = arg.value; } else { propitems = [arg.value] } } tempitem[prop] = propitems; } else if (aggregated in finalitem && Array.isArray(finalitem[aggregated]) && finalitem[aggregated].length > afteraggregated) { const propval = finalitem[aggregated][afteraggregated]; const mappedval = arg.subop?.endsWith('~') ? propval : [propval]; tempitem[prop] = mappedval; postitem[prop] = mappedval; afteraggregated++; } } }); if (op.operator == '....' && afteraggregated > 0) { outputlist.push(postitem); } else if (op.operator == '....' && finalvalue != null) { if (dtype(finalvalue, 'array')) { outputlist = outputlist.concat(finalvalue); } else { outputlist.push(finalvalue); } } else { for (const prop in tempitem) { if (!(prop in finalitem) && prop != 'of') finalitem[prop] = tempitem[prop]; } if (includeof) finalitem.of = tempitem.of; outputlist.push(finalitem); } } break; case '|': // annotate outputlist = currentlist.slice(0).map((item) => dcopy(item)); op.args.filter((arg) => arg.subop?.includes('>') && arg.label != null && arg.label != undefined && Array.isArray(arg.value)) .forEach((arg) => (labeled['=>'] ??= {})[arg.label] = arg.value); const annotatetimer = {}; op.args.filter((arg) => arg.subop?.endsWith('@')).forEach((arg) => arg.counter = undefined); for (let i = 0; i < outputlist.length; i++) { this.timecheck(annotatetimer, i, outputlist.length, op); const baseitem = outputlist[i]; if (typeof baseitem != 'object') { outputlist[i] = {}; if (op.args?.[0]?.value != '_') outputlist[i].name = baseitem; } const item = outputlist[i]; const newprops = op.args.filter((arg) => arg.subop != '<' && (arg.label || !arg.subop?.includes('-'))).map((arg) => arg.label || arg.value); if (typeof item == 'object') { let argx = -1; for (const arg of op.args) { argx++; if (arg?.subop == '<' && dtype(arg.value, 'string') && argx < op.args.length - 1) { if (arg.value in item && (Array.isArray(item[arg.value]) || dtype(item[arg.value], 'object'))) { const sublabeled = {...labeled}; if (arg.label) sublabeled[arg.label] = [item]; item[arg.value] = this.execute(item[arg.value], [{ operator: op.operator, args: op.args.slice(argx + 1).map(opclone) }], sublabeled); } break; } else if (arg.separator == ';' && argx == op.args.length - 1 && arg.label == null && arg.value == null && (arg.subop == null || arg.subop == '~')) { for (const oldprop in item) { if (!(newprops.includes(oldprop))) { const tempval = item[oldprop]; delete item[oldprop]; if (arg.subop != '~') item[oldprop] = tempval; } } } else if (arg.subop?.endsWith('-')) { if (arg.value && typeof arg.value == 'string' && arg.value in item) { if (arg.label && typeof arg.label == 'string') item[arg.label] = item[arg.value]; delete item[arg.value]; } else if (arg.label && Array.isArray(arg.value)) { const toremoveids = new Set(step(item, arg.value, null, labeled).map((subitem) => getid(subitem))); item[arg.label] = item[arg.label].filter((subitem) => !toremoveids.has(getid(subitem))); } } else if (argx == 0 && arg.label != null && arg.value == '_') { item[arg.label] = [baseitem]; } else { if (!arg.label && arg.value && typeof arg.value === 'string' && arg.value != '') { if (arg.subop == '=' && arg.value in this.annotators) { item[arg.value] = this.annotators[arg.value](item); } else if (arg.value in item) { const moveprop = arg.value; const value = item[moveprop]; delete item[moveprop]; item[moveprop] = value; } else if (arg.value in this.annotators) { item[arg.value] = this.annotators[arg.value](item); } } if (arg.label) { let vals = []; if (arg.subop.endsWith('@')) { if (arg.subop.endsWith('@@')) { arg.counter ??= arg.value == null ? outputlist.length + 1 : 0; } else { arg.counter ??= 1; vals = [arg.counter]; } if (Array.isArray(arg.value)) { arg.counter += step(item, arg.value, null, labeled).length; } else if (dtype(arg.value, 'string') && dtype(item[arg.value], 'number')) { arg.counter += Number(item[arg.value]); } else { arg.counter += arg.subop.endsWith('@@') ? -1 : 1; } if (arg.subop.endsWith('@@')) { vals = [arg.counter]; } } else if (arg.subop == '=' && typeof arg.value == 'string' && arg.value in this.annotators) { vals = [this.annotators[arg.value](item)]; } else { vals = step(item, arg.value, arg.subop, labeled); } const scalar = arg.subop?.includes('~') || arg.subop?.endsWith('@') || (arg.label != '_' && arg.subop == '=' && typeof arg.value == 'string' && (arg.value in this.annotators || arg.value.startsWith('=') || arg.value.startsWith('~'))); const base = (arg.subop?.endsWith('+') && arg.label in item && Array.isArray(item[arg.label])) ? item[arg.label] : []; if (arg.label == '_') { const sourceitem = dtype(vals, 'array') ? vals[0] : val; if (dtype(sourceitem, 'object')) { for (const prop in sourceitem) { if (!(prop in item)) { if (scalar && Array.isArray(sourceitem[prop])) { item[prop] = sourceitem[prop][0]; } else { item[prop] = sourceitem[prop]; } newprops.push(prop); } } } } else if (dtype(vals, 'array')) { if (scalar && vals.length > 0) { item[arg.label] = getname(vals[0]); } else { item[arg.label] = base.concat(vals.map((v) => dcopy(v))); } } else if (vals) { item[arg.label] = base.concat(vals); } } } } } } break; case '???': outputlist = currentlist; const commentval = op.args?.[0]?.value; if (commentval in labeled) console.log({[commentval]: labeled[commentval].slice(0)}); if (commentval?.startsWith('end')) { if (this.debug && !inputlist) { return operations; } else if (commentval.length > 3 && !isNaN(commentval.slice(3))) { return outputlist.slice(0, Number(commentval.slice(3))); } else { return outputlist; } } if (commentval == 'recache') this.recache = new Set(); if (typeof currentlist[0] == 'object' && op.args?.[0]?.value in currentlist[0] && !op.args?.[0]?.label) console.log({[op.args?.[0]?.value]: currentlist[0][op.args?.[0]?.value]}); break; default: outputlist = [] } currentlist = outputlist.slice(0); if (toplevel) { if (!this.adaptive) { op.completed = true; operations[0].progress = currentlist; operations[0].labeled = labeled; } if (this.debug) { op.results = outputlist.slice(0, typeof this.debug == 'number' ? this.debug : outputlist.length); op.time = (performance.now() - opstart) / 1000; } } } return (this.debug && !inputlistraw) ? operations : outputlist; } gettype(value) { if (value == null || value == undefined) return null; const trylist = [value]; if (this.features.plurality) { if (value.endsWith('s')) { trylist.push(value.slice(0, value.length - 1)); trylist.push(value + 'es'); } else { trylist.push(value + 's'); } } for (const tryval of trylist) { if (tryval in this.data) { return this.data[tryval]; } else if (this.internal_datasets.includes(tryval)) { return []; } } if (this.savedquerynames.has(value)) { const savedqueries = this.data.queries.filter((q) => q.name == value && !q.relative); if (savedqueries?.length == 1) { const sq = savedqueries[0]; if (!sq.results) sq.results = this.executeq(sq.query); return sq.results.slice(0); } } return null; } dcopy = (item) => { if (item === null || typeof item !== 'object') return item; if (Array.isArray(item)) return item.slice(); return {...item}; } dtype(item, test = null) { const t = typeof item; const type = t === 'object' ? (item === null ? null : Array.isArray(item) ? 'array' : 'object') : (t === 'string' || t === 'number' || t === 'boolean') ? 'literal' : null; if (!test) return type; if (test === 'number') return type === 'literal' && !isNaN(item); if (test === 'string') return t === 'string'; if (test === 'boolean') return t === 'boolean'; return type === test; } nullish(val) { return val === false || val === null || val === undefined || val === '' || (Array.isArray(val) && val.length == 0) || (typeof val == 'object' && Object.keys(val).length == 0); } getname = (item) => { if (typeof item === 'object' && item != null) { if (item?.name != null) { return item.name; } else if (item?.key?.length > 0) { return item.key.join(' / '); } if (this.features.guessname) { const {id, ...nonidprops} = item; const names = Object.values(nonidprops).filter((val) => typeof val === 'string' || typeof val === 'number'); if (names.length > 0) { return names[0]; } for (const key in nonidprops) { if (Array.isArray(item[key]) && item[key].length == 1 && typeof item[key][0] == 'string') { return item[key][0]; } } } } else if (typeof item === 'string' || typeof item === 'number') { return item; } else if (typeof item === 'boolean') { return item.toString(); } return ''; } getid = (item) => { if (typeof item === 'object' && item != null) { const iditems = (item.id || item.uri || item.name) ? [item] : (item.key?.length > 0 && !item.keyindex) ? item.key : Object.keys(item).length == 3 && item.of?.length > 0 ? item.of : [item]; return iditems.map((item) => { if (item.id) { if (Array.isArray(item.id)) { return item.id[0]; } else { return item.id; } } else if (item.uri) { if (Array.isArray(item.uri)) { return item.uri[0]; } else { return item.uri; } } else if (this.features.guessid && item.name) { return item.name; } else { const stringifiedid = JSON.stringify(item); // if (stringifiedid.length > 128) console.warn({idstringify: item, idlength: stringifiedid.length}); return stringifiedid; } }).join(','); } else if (typeof item === 'string' || typeof item === 'number') { return item; } return null; } getnumber = (val) => { let number = Array.isArray(val) ? val[0] : val; number = !isNaN(number) ? Number(val) : null; return number; } opclone = (x) => { if (x === null || typeof x !== 'object') return x; // primitives if (Array.isArray(x)) { const a = new Array(x.length); for (let i = 0; i < x.length; i++) a[i] = this.opclone(x[i]); return a; } if (x instanceof RegExp) return new RegExp(x.source, x.flags); // preserve the `~` matchers const o = {}; for (const k of Object.keys(x)) o[k] = this.opclone(x[k]); return o; } step = (item, property, subop, labeled) => { // (this.data.trace ??= []).push({stepitem: item, property: property, subop: subop, labeled: labeled}); const dtype = this.dtype; const dcopy = this.dcopy; const resolve = this.resolve; const escapeRegExp = this.escapeRegExp; const opclone = this.opclone; const vals = []; if (subop?.includes('~') && dtype(property, 'literal')) { vals.push(property); } else if (Array.isArray(property)) { const res = this.execute([item], property.map(opclone), labeled).flatMap((x) => x); if (subop?.includes('~') && res.length > 0) { vals.push(this.getname(res[0])); } else { res.forEach((val) => vals.push(val)); } } else if (property == '_') { vals.push(dcopy(item)); } else if (item?.of && dtype(property, 'number')) { const propval = Number(property); (subop == '-' || propval < 0 ? item.of.slice(-1 * Math.abs(Number(property))) : item.of.slice(0, Math.abs(Number(property)))).forEach((val) => vals.push(val)); } else if (property == 'id' && dtype(item, 'literal')) { vals.push(item); } else if (property == 'name') { vals.push(this.getname(item)); } else if (this.features.inlinemath && property.startsWith('=')) { let calculation = property.slice(1); const mathwords = Object.getOwnPropertyNames(Math).filter((mathword) => mathword.match(/^[a-z0-9]+$/)).sort((a, b) => b.length - a.length || a.localeCompare(b)); const otherwords = ['split']; const variables = Object.entries(item).concat(Object.entries(labeled)) .map(([k, v]) => k) .sort((a, b) => b.length - a.length || a.localeCompare(b)); if (dtype(this.getname(item), 'number')) variables.push('_'); const allowedwords = mathwords.concat(otherwords).concat(variables).sort((a, b) => b.length - a.length || a.localeCompare(b)); const allowed = new RegExp(`^((\\b(${allowedwords.map((w) => escapeRegExp(w)).join('|')})\\b)|([0-9_\\+\\/\\*\\(\\)\\[\\]\\.%=,'" -]*))*$`); let val; if (calculation.match(allowed)) { for (const variable of variables) { const variableex = new RegExp(`\\b${escapeRegExp(variable)}\\b`, 'g'); if (calculation.match(variableex)) { let vval = variable == '_' ? Number(this.getname(item)) : item[variable] ?? labeled[variable]; if (Array.isArray(vval) && vval.length == 1) vval = vval[0]; calculation = calculation.replaceAll(variableex, ` ${dtype(vval, 'number') ? vval : JSON.stringify(vval)} `); } } if (calculation.match(/[A-Za-z]/)) { for (const mathword of mathwords) { const mathwordex = new RegExp(`\\b${mathword}\\b`, 'g'); calculation = calculation.replaceAll(mathwordex, `Math.${mathword}`); if (!calculation.match(/[A-Za-z]/)) break; } } calculation = calculation.replaceAll(/\b=\b/g, '=='); try { val = eval(calculation); } catch (error) { val = calculation; } } else { val = calculation; } if (val !== false) vals.push(val); } else if (property.startsWith('~')) { let val = property.slice(1); const variables = Object.entries(item).concat(Object.entries(labeled)) .map(([k, v]) => k) .sort((a, b) => b.length - a.length || a.localeCompare(b)); variables.push('_'); for (const variable of variables) { const variableex = new RegExp(`(? vals.push(x)); } else { vals.push(resolved); } found = true; break; } } } if (!found && labeled?.['=>']?.[property]) { this.execute([item], labeled['=>'][property], labeled).forEach((val) => vals.push(val)); found = true; } if (!found && (property in this.data || property in this.adapters || this.savedquerynames.has(property) || property in labeled)) { const typenav = resolve(property, item, labeled, true); if (typenav) { (Array.isArray(typenav) ? typenav : [typenav]).forEach((t) => vals.push(t)); found = true; } } if (!found && property.includes('→')) { const parts = property.split(/\s*→\s*/).filter((part) => part?.length > 0); if (dtype(item, 'object')) { this.step(item, parts.map((part) => ({ operator: '..', args: [{value: part}] }))).forEach((val) => vals.push(val)); } else { const asnum = dtype(item, 'number'); if (item == (asnum ? Number(parts[0]) : parts[0])) { parts.slice(1).forEach((newval) => vals.push(asnum ? Number(newval) : newval)); } else { vals.push(item); } } } } return vals; } indexlogit = (property, type) => { const today = new Date().toISOString().slice(0, 10); ((this.index._ ||= {})[property] ||= {})[today] ||= {read: 0, write: 0}; this.index._[property][today][type]++; this.index_modified.add('_'); if (type == 'write') this.index_modified.add(property); } lookup = (item, list) => { return list.filter((li) => { if (item.id) return item.id == li.id; if (item.uri) return item.uri == li.uri; for (const k of this.kkeys(item)) { if (item[k]?.toString() != li?.[k]?.toString()) return false; } return true; }); } indexed = (property, key, item, func) => { if ((!this.recache || this.recache.has(property)) && this.index[property]?.[key] !== undefined) { this.indexlogit(property, 'read'); return this.index[property][key]; } const res = func(item); if (res) { (this.index[property] ||= {})[key] = res; this.indexlogit(property, 'write'); } return res; } resolve = (property, item, labeled, navigate = false) => { const dtype = this.dtype; const aqueue = this.aqueue; if (dtype(item, 'object')) { if (this.adapters?.[property]?.annotator) { return this.indexed(property, this.getid(item), item, () => aqueue(property, item)); } else if (navigate) { const res = this.indexed(property, this.getid(item), item, () => { if (property in this.data) { return this.lookup(item, this.data[property]); } else if (property in labeled) { return this.lookup(item, labeled[property]); } else if (this.savedquerynames.has(property)) { const saved_query = this.data.queries.find((q) => q.name == property); if (saved_query.results && !saved_query.relative) { return this.lookup(item, saved_query.results); } else if (saved_query.relative) { return this.execute([item], this.parse(saved_query.query), labeled); } } return null; }); if (res) return res; if ('id' in item || 'uri' in item) { return this.resolve(property, this.getid(item), labeled); } return null; } else { return item; } } else if (dtype(item, 'literal') && typeof property == 'string' && (property in labeled || this.destinations.has(property) || (this.features.plurality && !this.internal_datasets.includes(property + 's') && this.destinations.has(property + 's')))) { let dataset = false; const res = this.indexed(property, item, item, () => { if (!(this.features.autoresolve || navigate)) return null; const typeitems = labeled[property] ?? this.gettype(property); if (Array.isArray(typeitems)) { dataset = true; const trykeys = ['id', 'uri', 'name']; if (this.features.plurality) { trykeys.push(property); trykeys.push(property.endsWith('s') ? property.slice(0, property.length - 1) : property + 's'); } for (const lookupkey of trykeys) { const found = typeitems.filter((typeitem) => { return dtype(typeitem, 'object') && (lookupkey in typeitem) && (typeitem[lookupkey].toString() == item || (Array.isArray(typeitem[lookupkey]) && typeitem[lookupkey].length == 1 && typeitem[lookupkey][0].toString() == item)); }); if (found.length == 1) { return found[0]; } } return null; } else if (this.savedquerynames.has(property)) { const saved_query = this.data.queries.find((q) => q.name == property); if (saved_query.results && !saved_query.relative) { return this.lookup(item, saved_query.results); } else if (saved_query.relative) { return this.execute([item], this.parse(saved_query.query), labeled); } } return null; }); if (res) return res; if (dataset) { if (property in this.adapters && (!this.recache || this.recache.has(property))) { return aqueue(property, item); } return null; } if (property in this.adapters) { return aqueue(property, item); } } return item; } aqueue = (property, item) => { const pending_queues = Object.keys(this.adapters).filter((key) => this.adapters[key].queue.length > 0); if (pending_queues.length == 0 || (pending_queues.length == 1 && pending_queues[0] == property)) { this.adapters[property].queue.push(this.adapters[property].annotator ? item : this.getid(item)); } this.adaptive = true; return undefined; } samearray(a, b) { if ((a && !b) || (!a && b) || a.length != b.length) return false; for (let x = 0; x < a.length; x++) { if (this.getid(a[x]) != this.getid(b[x])) return false; } return true; } verify = async (i = null) => { let toverify = this.data.queries; if (i && !isNaN(i)) toverify = toverify.slice(i - 1, i); let allsame = true; for (const savedq of toverify) { console.log('verifying ' + savedq.name); const testres = await this.query(savedq.query); if (testres.length != savedq.results.length) { console.log('--x result count changed from ' + savedq.results.length + ' to ' + testres.length); allsame = false; } else { for (let i = 0; i < testres.length; i++) { const testrow = testres[i]; const savedrow = savedq.results[i]; if (typeof savedrow == 'object') { for (const prop in savedrow) { if (!(prop in testrow)) { console.log('--x row ' + (i + 1) + ': new results missing property ' + prop); allsame = false; } else { const testval = JSON.stringify(testrow[prop]); const savedval = JSON.stringify(savedrow[prop]); if (testval != savedval) { console.log('--x row ' + (i + 1) + ': different value for property ' + prop); console.log({was: savedrow[prop], now: testrow[prop]}) allsame = false; } } } } else { if (testrow != savedrow) { console.log('--x row ' + (i + 1) + ': different value'); console.log({was: savedrow, now: testrow}); allsame = false; } } } } if (allsame) console.log('--- results unchanged'); } } index_check() { console.table(Object.entries(this.index).map(([key, vals]) => ({ key: key, vals: Object.keys(vals).length, size: Object.keys(vals).length * JSON.stringify(Object.entries(vals).slice(0, 1)).length })).sort((a, b) => b.size - a.size || b.vals - a.vals || a.key.localeCompare(b.key))) } index_materialize() { this.load(Object.keys(this.index).flatMap((i) => Object.keys(this.index[i]).flatMap((k) => ({ index: i, indexed: k, value: this.index[i][k] }))), 'index contents') } indexlog_materialize() { this.load(Object.entries(this.index._).map(([k, v]) => ({ index: k, log: Object.entries(v).map(([date, readwrite]) => ({ date: date, read: readwrite.read, write: readwrite.write })) })), 'indexlog'); } queries_check() { console.table(dactal.data.queries.map((q) => ({ queryname: q.name, results: q.results?.length || 0, size: q.results?.length > 0 ? q.results.length * JSON.stringify(q.results[0]).length : 0 })).sort((a, b) => b.size - a.size || b.results - a.results || a.queryname.localeCompare(b.queryname))) } data_check() { console.table(Object.entries(this.data).map(([key, vals]) => ({ key: key, vals: Object.keys(vals).length, size: Object.keys(vals).length * JSON.stringify(Object.entries(vals).slice(0, 1)).length })).sort((a, b) => b.size - a.size || b.vals - a.vals || a.key.localeCompare(b.key))) } } window.DACTAL = new DACTAL();