Source: array2string.mjs

import {
	tester,
	arange,
	array,
	asarray,
	NDArray,
	index_exp,
	concatenate,
	Slice,
	shallow_array_equal,
	guessType,
	dtype_,
} from './core.mjs';

/**
 *
 * @param {NDArray} a
 * @param {FormatOptions} options
 * @returns {string}
 */
export function array2string(a, options = null) {
	options = options != null ? Object.assign(Object.create(_formatOptions), options) : _formatOptions;

	let { separator, prefix } = options;

	if (a.size == 0) return `[]`;

	return _array2string(a, options, separator, prefix);
}

/**
 *
 * @param {NDArray} a
 * @param {number} linewidth
 * @param {number} precision
 * @returns {string}
 */
export function array_str(a, linewidth = null, precision = null) {
	linewidth ??= _formatOptions.linewidth;
	precision ??= _formatOptions.precision;

	if (a.ndim == 0) return `${a.item()}`;

	return array2string(a, { linewidth, precision });
}

/**
 *
 * @param {NDArray} a
 * @param {number} linewidth
 * @param {number} precision
 * @returns {string}
 */
export function array_repr(a, linewidth = null, precision = null) {
	linewidth ??= _formatOptions.linewidth;
	precision ??= _formatOptions.precision;

	if (a.ndim == 0) return `${a.item()}`;

	let class_name = a instanceof NDArray ? 'array' : typeof a;

	let skipdtype = dtype_is_implied(a.dtype) && a.size > 0;

	let prefix = class_name + '(';
	let suffix = skipdtype ? ')' : ',';

	let lst;
	if (a.size > 0 || shallow_array_equal(a.shape, [0])) {
		lst = array2string(a, { linewidth, precision, separator: ', ', prefix });
	} else {
		lst = `[], shape=[${a.shape.join(', ')}]`;
	}

	let arr_str = prefix + lst + suffix;

	if (skipdtype) return arr_str;

	let dtype_str = `dtype=${a.dtype.name})`;

	let last_line_len = arr_str.length - (arr_str.lastIndexOf('\n') + 1);
	let spacer = ' ';

	if (last_line_len + dtype_str.length + 1 > linewidth) {
		spacer = '\n' + ' '.repeat((class_name + '(').length);
	}

	return arr_str + spacer + dtype_str;
}

/**
 * @typedef {Object} FormatOptions
 * @property {number} edgeitems
 * @property {number} threshold
 * @property {number|undefined} precision
 * @property {number} linewidth
 * @property {number|null} formatter
 * @property {string} separator
 * @property {string} prefix
 */

/**
 * @type {FormatOptions}
 * @ignore
 */
let _formatOptions = {
	edgeitems: 3,
	threshold: 1000,
	linewidth: 75,
	precision: undefined,
	formatter: null,
	separator: ' ',
	prefix: '',
};

/**
 *
 * @param {NDArray} a
 * @param {number} edgeitems
 * @param {Array<number|Slice>} index
 * @returns {NDArray}
 * @ignore
 */
function _leading_trailing(a, edgeitems, index = []) {
	let axis = index.length;
	if (axis == a.ndim) return a.get(index);

	if (a.shape[axis] > 2 * edgeitems) {
		let leftIndex = index.concat(index_exp([, edgeitems]));
		let rightIndex = index.concat(index_exp([-edgeitems]));
		return concatenate(
			[_leading_trailing(a, edgeitems, leftIndex), _leading_trailing(a, edgeitems, rightIndex)],
			axis
		);
	}

	return _leading_trailing(a, edgeitems, index.concat(index_exp(':')));
}

/**
 * @class
 * @ignore
 */
class Callable {
	get __call__() {
		return this.call.bind(null, this);
	}
}

/**
 * @class
 * @ignore
 */
class IntegerFormat extends Callable {
	/**
	 *
	 * @param {NDArray} data
	 */
	constructor(data) {
		super();
		this.padLeft = data.size > 0 ? Math.max(String(data.max()).length, String(data.min()).length) : 0;
	}

	/**
	 *
	 * @param {{padLeft: number}} thisArg
	 * @param {number} x
	 */
	call({ padLeft } = this, x) {
		let str = String(x);
		return ' '.repeat(padLeft - str.length) + str;
	}
}

/**
 *
 * @param {number} x
 * @param {number|undefined} precision
 * @returns {string}
 * @ignore
 */
function scientific(x, precision) {
	return x.toExponential(precision);
}

/**
 *
 * @param {number} x
 * @param {number|undefined} precision
 * @returns {string}
 * @ignore
 */
function positional(x, precision) {
	return precision != undefined ? x.toFixed(precision) : String(x);
}

/**
 * @class
 * @ignore
 */
class FloatingFormat extends Callable {
	/**
	 *
	 * @param {NDArray} data
	 * @param {number} precision
	 */
	constructor(data, precision = undefined) {
		super();
		this.precision = precision;

		let exp_format = false;
		{
			let min_val = data.item(0);
			let max_val = min_val;
			let any_non_zero_finite = false;

			for (let x of data.flat) {
				if (Number.isFinite(x)) {
					if (x != 0) {
						x = Math.abs(x);
						min_val = Math.min(min_val, x);
						max_val = Math.max(max_val, x);
						any_non_zero_finite = true;
					}
				}
			}

			if (any_non_zero_finite && (max_val >= 1e8 || min_val < 0.0001 || max_val / min_val > 1000)) {
				exp_format = true;
			}
		}
		this.exp_format = exp_format;

		let max_len = 0;
		if (this.exp_format) {
			for (let x of data.flat) {
				max_len = Math.max(max_len, scientific(x, precision).length);
			}
		} else {
			for (let x of data.flat) {
				max_len = Math.max(max_len, positional(x, precision).length);
			}
		}
		this.padLeft = max_len;
	}

	/**
	 *
	 * @param {{padLeft: number, exp_format: boolean, precision: number|undefined}} thisArg
	 * @param {number} x
	 */
	call({ padLeft, exp_format, precision } = this, x) {
		let str = exp_format ? scientific(x, precision) : positional(x, precision);
		return ' '.repeat(padLeft - str.length) + str;
	}
}

/**
 * @param {*} x
 * @ignore
 */
function default_format(x) {
	if (typeof x == 'string') return `'${x}'`;
	return `${x}`;
}

/**
 * @param {*} x
 * @ignore
 */
let indirect = x => () => x;

/**
 * @param {NDArray} data
 * @param {FormatOptions} options
 * @ignore
 */
function _get_formatdict(data, options) {
	let formatdict = {
		int: () => new IntegerFormat(data).__call__,
		float: () => new FloatingFormat(data, options.precision).__call__,
		object: () => default_format,
	};

	let { formatter } = options;

	if (formatter != null) {
		for (let key of Object.keys(formatter)) {
			formatdict[key] = indirect(formatter[key]);
		}
	}

	return formatdict;
}

function all_integer(array) {
	for (let n of array) {
		if (!Number.isInteger(n)) return false;
	}
	return true;
}

function _get_format_function(data, options) {
	let formatdict = _get_formatdict(data, options);
	let array = data.flat.copy().data;
	let type = guessType(array);
	let dtype = dtype_(type) == dtype_('number') ? (all_integer(array) ? 'int' : 'float') : 'object';
	return formatdict[dtype](options);
}

/**
 * @param {string} s
 * @param {string} line
 * @param {string} word
 * @param {number} line_width
 * @param {string} next_line_prefix
 * @param {number} legacy
 * @returns {string[]}
 * @ignore
 */
function _extendLine(s, line, word, line_width, next_line_prefix, legacy) {
	let needs_wrap = line.length + word.length > line_width;
	if (legacy > 113) {
		if (line.length <= next_line_prefix.length) {
			needs_wrap = false;
		}
	}

	if (needs_wrap) {
		s += line.trimEnd() + '\n';
		line = next_line_prefix;
	}
	line += word;
	return [s, line];
}

/**
 * @param {string} s
 * @param {string} line
 * @param {string} word
 * @param {number} line_width
 * @param {string} next_line_prefix
 * @param {number} legacy
 * @returns {string[]}
 * @ignore
 */
function _extendLine_pretty(s, line, word, line_width, next_line_prefix, legacy) {
	let words = word.split(/\r?\n/);
	if (words.length === 1 || legacy <= 113) {
		return _extendLine(s, line, word, line_width, next_line_prefix, legacy);
	}

	let max_word_length = Math.max(...words.map(w => w.length));
	let indent;
	if (line.length + max_word_length > line_width && line.length > next_line_prefix.length) {
		s += line.trimEnd() + '\n';
		line = next_line_prefix + words[0];
		indent = next_line_prefix;
	} else {
		indent = ' '.repeat(line.length);
		line += words[0];
	}

	for (let i = 1; i < words.length; i++) {
		s += line.trimEnd() + '\n';
		line = indent + words[i];
	}

	let suffix_length = max_word_length - words.at(-1).length;
	line += ' '.repeat(suffix_length);

	return [s, line];
}

function _formatArray(
	a,
	format_function,
	line_width,
	next_line_prefix,
	separator,
	edge_items,
	summary_insert,
	legacy
) {
	/**
	 *
	 * @param {Array<number|Slice>} index
	 * @param {string} hanging_indent
	 * @param {number} curr_width
	 * @returns {string}
	 */
	function recurser(index, hanging_indent, curr_width) {
		let axis = index.length;
		let axes_left = a.ndim - axis;

		if (axes_left === 0) {
			return format_function(a.item(index));
		}

		let next_hanging_indent = hanging_indent + ' ';
		let next_width;
		if (legacy <= 113) {
			next_width = curr_width;
		} else {
			next_width = curr_width - ']'.length;
		}

		let a_len = a.shape[axis];
		let show_summary = summary_insert && 2 * edge_items < a_len;
		let leading_items, trailing_items;

		if (show_summary) {
			leading_items = edge_items;
			trailing_items = edge_items;
		} else {
			leading_items = 0;
			trailing_items = a_len;
		}

		let s = '';

		if (axes_left === 1) {
			let elem_width;
			let seqlen = separator.trimEnd().length;
			if (legacy <= 113) {
				elem_width = curr_width - seqlen;
			} else {
				elem_width = curr_width - Math.max(seqlen, ']'.length);
			}

			let line = hanging_indent;
			for (let i = 0; i < leading_items; i++) {
				let word = recurser([...index, i], next_hanging_indent, next_width);
				[s, line] = _extendLine_pretty(s, line, word, elem_width, hanging_indent, legacy);
				line += separator;
			}

			if (show_summary) {
				[s, line] = _extendLine(s, line, summary_insert, elem_width, hanging_indent, legacy);
				if (legacy <= 113) line += ', ';
				else line += separator;
			}

			for (let i = trailing_items; i > 1; i--) {
				let word = recurser([...index, -i], next_hanging_indent, next_width);
				[s, line] = _extendLine_pretty(s, line, word, elem_width, hanging_indent, legacy);
				line += separator;
			}

			if (legacy <= 113) elem_width = curr_width;
			let word = recurser([...index, -1], next_hanging_indent, next_width);
			[s, line] = _extendLine_pretty(s, line, word, elem_width, hanging_indent, legacy);

			s += line;
		} else {
			s = '';
			let line_sep = separator.trimEnd() + '\n'.repeat(axes_left - 1);

			for (let i = 0; i < leading_items; i++) {
				let nested = recurser([...index, i], next_hanging_indent, next_width);
				s += hanging_indent + nested + line_sep;
			}

			if (show_summary) {
				s += hanging_indent + summary_insert;
				if (legacy <= 113) s += ', \n';
				else s += line_sep;
			}

			let i = trailing_items;
			for (; i > 1; i--) {
				let nested = recurser([...index, -i], next_hanging_indent, next_width);
				s += hanging_indent + nested + line_sep;
			}

			let nested = recurser([...index, -i], next_hanging_indent, next_width);
			s += hanging_indent + nested;
		}

		s = '[' + s.slice(hanging_indent.length) + ']';
		return s;
	}

	try {
		return recurser([], next_line_prefix, line_width);
	} finally {
		recurser = null;
	}
}

/**
 * @param {NDArray} a
 * @param {FormatOptions} options
 * @param {string} separator
 * @param {string} prefix
 * @returns {string}
 * @ignore
 */
function _array2string(a, options = _formatOptions, separator = ' ', prefix = '') {
	let data = asarray(a);
	if (a.ndim == 0) a = data;

	let summary_insert;
	if (a.size > options.threshold) {
		summary_insert = '...';
		data = _leading_trailing(data, options.edgeitems);
	} else {
		summary_insert = '';
	}

	let format_function = _get_format_function(data, options);

	let next_line_prefix = ' ' + ' '.repeat(prefix.length);

	let lst = _formatArray(
		a,
		format_function,
		options.linewidth,
		next_line_prefix,
		separator,
		options.edgeitems,
		summary_insert,
		options.legacy
	);

	return lst;
}

function dtype_is_implied(dtype) {
	return ['number', 'boolean'].includes(dtype.name);
}

process.env.PRODUCTION ||
	tester
		.add(
			array2string,
			() => array2string(arange(3 * 4 * 5 * 6 * 7).reshape(3, 4, 5 * 6 * 7)),
			() =>
				'[[[   0    1    2 ...  207  208  209]\n  [ 210  211  212 ...  417  418  419]\n  [ 420  421  422 ...  627  628  629]\n  [ 630  631  632 ...  837  838  839]]\n\n [[ 840  841  842 ... 1047 1048 1049]\n  [1050 1051 1052 ... 1257 1258 1259]\n  [1260 1261 1262 ... 1467 1468 1469]\n  [1470 1471 1472 ... 1677 1678 1679]]\n\n [[1680 1681 1682 ... 1887 1888 1889]\n  [1890 1891 1892 ... 2097 2098 2099]\n  [2100 2101 2102 ... 2307 2308 2309]\n  [2310 2311 2312 ... 2517 2518 2519]]]'
		)
		.add(
			array2string,
			() => array2string(array([0.1, 100, 50, -9000])),
			() => '[ 1e-1  1e+2  5e+1 -9e+3]'
		)
		.add(
			array2string,
			() => array2string(array([1.1, 100.2, 50.6])),
			() => '[  1.1 100.2  50.6]'
		);

process.env.PRODUCTION ||
	tester
		.add(
			array_repr,
			() => array_repr(array([1.1, 100.2, 50.6])),
			() => 'array([  1.1, 100.2,  50.6])'
		)
		.add(
			array_repr,
			() => '' + array([1.1, 100.2, 50.6]),
			() => 'array([  1.1, 100.2,  50.6])'
		);

// tester.onload(() => {
// 	console.log(array([10.22, 12.9, 66.3]).valueOf());
// 	console.log(array(99));
// });