/*
* This file is part of TREB.
*
* TREB is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* TREB is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along
* with TREB. If not, see .
*
* Copyright 2022-2026 trebco, llc.
* info@treb.app
*
*/
import type { ArrayUnion, CellValue, UnionValue} from 'treb-base-types';
import { ValueType } from 'treb-base-types';
export const DAY_MS = 1000 * 60 * 60 * 24;
export const IsArrayOrTypedArray = (test: unknown): boolean => {
return Array.isArray(test) || (test instanceof Float64Array) || (test instanceof Float64Array);
};
export const Transpose2 = (arr: T[][]): T[][] => {
const result: T[][] = [];
const cols = arr.length;
const rows = arr[0].length;
for (let r = 0; r < rows; r++) {
result[r] = [];
for (let c = 0; c < cols; c++ ) {
result[r][c] = arr[c][r];
}
}
return result;
};
export const TransposeArray = (arr: T[][]): T[][] => {
if (!arr) return [];
if (typeof arr[0] === 'undefined') return [];
/*
if (!IsArrayOrTypedArray(arr[0])) {
if (arr instanceof Float32Array || arr instanceof Float64Array){
return Array.prototype.slice.call(arr).map((x: T) => [x]);
}
return arr.map((x) => [x]);
}
*/
const tmp: T[][] = [];
const cols = arr.length;
const rows = arr[0].length;
for (let r = 0; r < rows; r++) {
tmp[r] = [];
for (let c = 0; c < cols; c++ ) {
tmp[r][c] = arr[c][r];
}
}
return tmp;
};
export const StringToColumn = (s: string) => {
let index = 0;
s = s.toUpperCase();
for (let i = 0; i < s.length; i++) {
index *= 26;
index += (s.charCodeAt(i) - 64);
}
return index - 1;
};
export const ColumnToString = (column: number) => {
// there's some weird case where this hangs, not sure
// how that happens. trap it and figure it out.
const original = column;
let s = '';
for (;;) {
const c = column % 26;
s = String.fromCharCode(65 + c) + s;
column = Math.floor(column / 26);
if (column) column--;
if (column < 0) throw(new Error('Column < 0!, original was ' + original));
else break;
}
return s;
};
export const OffsetFormula = (formula: string, offset: {columns: number, rows: number}) => {
const cache: Record = {};
formula = formula.replace(/\b([A-Za-z]+)(\d+)\b/g, (m, p1, p2) => {
if (!cache[m]) {
const c = ColumnToString(StringToColumn(p1) + offset.columns);
const r = Number(p2) + offset.rows;
cache[m] = c + r.toString();
}
return cache[m];
});
return formula;
};
/**
* assuming boxed (union) arguments, return a flat list of values.
* @param args
*/
export const FlattenBoxed = (args: UnionValue[]): UnionValue[] => {
// let's go back to imperative style for now
let result: UnionValue[] = [];
for (const arg of args) {
if (arg?.type === ValueType.array) {
// possibly recursive
for (const row of arg.value) {
// what's faster, concatenate or ...push?
// result.push(...FlattenBoxed(row));
result = result.concat(FlattenBoxed(row));
}
}
else {
result.push(arg);
}
}
return result;
};
/* *
* flatten a set of arguments
* UPDATE: we no longer accept the "arguments" object. must be an array.
* callers can use rest spread to collect arguments.
*
* @deprecated - use a better-typed version
* /
export const FlattenCellValues = (args: any[]): any[] => {
if (!Array.isArray(args)) { return [args]; } // special case
return args.reduce((a: any[], b: any) => {
if (typeof b === 'undefined') return a;
if (Array.isArray(b)) return a.concat(FlattenCellValues(b));
if (b instanceof Float32Array) return a.concat(Array.from(b));
if (b instanceof Float64Array) return a.concat(Array.from(b));
return a.concat([b]);
}, []);
};
*/
/**
* specialization using the CellValue type. this should be preferable
* to using any, although we're still kind of hacking at it. also we
* need to allow typed arrays (I think we've mostly gotten those out?)
*/
export const FlattenCellValues = (args: (CellValue|CellValue[]|CellValue[][]|Float32Array|Float64Array)[], keep_undefined = false): CellValue[] => {
if (!Array.isArray(args)) { return [args]; } // special case
return args.reduce((a: CellValue[], b) => {
if (typeof b === 'undefined' && !keep_undefined) return a;
if (Array.isArray(b)) return a.concat(FlattenCellValues(b, keep_undefined));
if (b instanceof Float32Array) return a.concat(Array.from(b));
if (b instanceof Float64Array) return a.concat(Array.from(b));
return a.concat([b]);
}, []);
};
/**
* flatten cell values, and filter out any non-numbers. this version does
* not account for booleans (you might want TRUE = 1). we do this a lot so
* combining the two operations seems like a useful place to reuse code.
*/
export const FlattenNumbers = (args: Parameters[0]) =>
(FlattenCellValues(args)).filter((value): value is number => typeof value === 'number');
export const FilterIntrinsics = (data: unknown[], fill = false): (string|number|boolean|undefined)[] => {
if (fill) {
return data.map((value => {
switch (typeof value) {
case 'number':
case 'undefined':
case 'string':
case 'boolean':
return value;
}
return undefined;
}));
}
return data.filter((value): value is number|boolean|undefined|string => {
switch (typeof value) {
case 'number':
case 'undefined':
case 'string':
case 'boolean':
return true;
}
return false;
});
};
/*
// anyone using this? (...) removing
export const UndefinedToEmptyString = (args: any[]): any[] => {
for (let i = 0; i < args.length; i++) {
if (Array.isArray(args[i])) {
args[i] = UndefinedToEmptyString(args[i]);
}
else if (typeof args[i] === 'undefined') {
args[i] = '';
}
}
return args;
};
*/
/** type guard. FIXME: move */
const IsArrayUnion = (value: unknown): value is ArrayUnion => {
return (!!value && typeof value === 'object' && (value as { type: ValueType, value?: Array }).type === ValueType.array);
};
/**
* this is an attempt at a generalized array application wrapper
* for functions. the rules are the same -- we apply functions pairwise
* (or tuple-wise), not combinatorially.
*
* we could simplify a bit if we require that functions used boxed
* values. we should do that anyway, and be consistent across functions.
* also we could get the `any`s out.
*
* (swapping param order, so we don't have hanging param after inline
* function definition -- shades of window.setTimeout)
*
* @param map - a list of parameters, by index, that can be unrolled. this
* allows us to support both out-of-order application and functions that take
* mixes of scalars and arrays.
*
* @param base - the underlying function. for any parameters that can
* be applied/unrolled, it should take a scalar.
*
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const ApplyArrayX = UnionValue>(map: boolean[], base: TFunc): (...args: Parameters) => UnionValue => {
return (...args: Parameters) => {
// we need to look at the actual parameters passed, not the signature.
// spreadsheet language supports "extra" parameters. we usually just
// repeat the tail parameter, but in this case we will NOT repeat the
// application (why not?)
const arrays: unknown[][][] = [];
// the result needs to be the same shape as the input. so we use the
// first array we find to create this map.
let shape: unknown[][] = [];
for (const [i, arg] of args.entries()) {
if (arg && map[i]) {
const arr = Array.isArray(arg) ? arg : IsArrayUnion(arg) ? arg.value : undefined;
if (arr) {
arrays[i] = arr;
if (!shape.length) {
shape = arr;
}
}
}
}
if (arrays.length) {
// this is pretty, but 3 functional loops? ouch
return {
type: ValueType.array,
value: shape.map((_, i) => _.map((_, j) => {
// this was breaking on boolean false, because
// it used || instead of ??. if we don't require
// boxed arguments, we need to handle naked booleans
// actually, this is still slightly wrong. we know (actually the
// caller of this function knows) if each argument is boxed or not,
// and if it's not, we should not return the boxed undefined type,
// we should just return undefined.
// that actually would basically solve false booleans as well, even
// if it were incorrectly returning undefined. the issue is we were
// returning a boxed undefined, which evaluates to true.
const apply = args.map((arg, index) => arrays[index] ? (arrays[index][i][j] ?? { type: ValueType.undefined }) : arg);
return base(...apply as Parameters);
})),
};
}
// scalar case
return base(...args);
};
};
/**
* parse a string with wildcards into a regex pattern
*
* from
* https://exceljet.net/glossary/wildcard
*
* Excel has 3 wildcards you can use in your formulas:
*
* Asterisk (*) - zero or more characters
* Question mark (?) - any one character
* Tilde (~) - escape for literal character (~*) a literal question mark (~?), or a literal tilde (~~)
*
* they're pretty liberal with escaping, nothing is an error, just roll with it
*
*/
export const ParseWildcards = (text: string): string => {
const result: string[] = [];
const length = text.length;
const escaped_chars = '[\\^$.|?*+()';
for (let i = 0; i < length; i++) {
let char = text[i];
switch (char) {
case '*':
result.push('.', '*');
break;
case '?':
result.push('.');
break;
case '~':
char = text[++i] || '';
// eslint-disable-next-line no-fallthrough
default:
for (let j = 0; j < escaped_chars.length; j++) {
if (char === escaped_chars[j]) {
result.push('\\');
break;
}
}
result.push(char);
break;
}
}
return result.join('');
};
export const StringUnion = (value: string): UnionValue => ({ type: ValueType.string, value });
export const NumberUnion = (value: number): UnionValue => ({ type: ValueType.number, value });