UNPKG

musicvis-lib

Version:

Music analysis and visualization library

409 lines (391 loc) 11.3 kB
/** * @module utils/ArrayUtils */ import * as d3 from 'd3' /** * Shallow compares two arrays * * @param {Array} a an array * @param {Array} b another array * @returns {boolean} true iff equal */ export function arrayShallowEquals (a, b) { if (a.length !== b.length) { return false } for (const [index, element] of a.entries()) { if (element !== b[index]) { return false } } return true } /** * Checks if two arrays contain the same elements, * ignoring their ordering in each array. * * @param {Array} a an array * @param {Array} b another array * @param {boolean} checkLength also checks if arrays have the same length * @returns {boolean} true iff arrays contain same elements */ export function arrayHasSameElements (a, b, checkLength = true) { if (checkLength && a.length !== b.length) { return false } const setA = new Set(a) const setB = new Set(b) for (const element of setA) { if (!setB.has(element)) { return false } } for (const element of setB) { if (!setA.has(element)) { return false } } return true } /** * Counts how often value appears in array * @todo use the one from mvlib, already moved there * @param {Array} array array * @param {*} value value * @param {function} [comparator] comparator function, can be undefined to just * use ===. Comparator has to return true when values are regarded as equal * @returns {number} count */ export function count (array, value, comparator) { let count = 0 // if (!array || (Array.isArray(array))) { // throw new Error('Undefined or invalid array') // } if (!comparator) { // Use === for (const v of array) { if (v === value) { count++ } } } else { // Use comparator function for (const v of array) { if (comparator(v, value)) { count++ } } } return count } /** * Jaccard index calulates the similarity of the sets as the size of the set * interaction divided by the size of the set union: * jackard_index = |intersection| / |union| * * @see https://en.wikipedia.org/wiki/Jaccard_index * @param {number[]} set1 set 1 * @param {number[]} set2 set 2 * @returns {number} similarity in [0, 1] */ export function jaccardIndex (set1, set2) { if (set1.length === 0 && set2.length === 0) { return 1 } return d3.intersection(set1, set2).size / d3.union(set1, set2).size } /** * Kendall Tau distance * * @see https://en.wikipedia.org/wiki/Kendall_tau_distance * @todo naive implementation, can be sped up with hints on Wikipedia, * see also https://stackoverflow.com/questions/6523712/calculating-the-number-of-inversions-in-a-permutation/6523781#6523781 * @param {number[]} ranking1 a ranking, i.e. for each entry the rank * @param {number[]} ranking2 a ranking, i.e. for each entry the rank * @param {boolean} [normalize=true] normalize to [0, 1]? * @returns {number} Kendall tau distance * @throws {'Ranking length must be equal'} if rankings don't have euqal length */ export function kendallTau (ranking1, ranking2, normalize = true) { if (ranking1.length !== ranking2.length) { throw new Error('Ranking length must be equal') } if (ranking1.length === 0) { return 0 } let inversions = 0 const n = ranking1.length for (let a = 0; a < n; a++) { for (let b = a + 1; b < n; b++) { const r1smaller = ranking1[a] < ranking1[b] const r2smaller = ranking2[a] < ranking2[b] if (r1smaller !== r2smaller) { inversions++ } } } if (normalize) { inversions /= n * (n - 1) / 2 } return inversions } /** * Removes duplicates from an Array by converting to a Set and back * * @param {Array} array an array * @returns {Array} array without duplicates */ export function removeDuplicates (array) { return [...new Set(array)] } /** * Checks whether the array a contains the array b, i.e. whether the first * b.length elements are the same. * * @todo rename to arrayStartsWithArray * @param {Array} a an array * @param {Array} b a shorter array * @returns {boolean} true iff a contains b */ export function arrayContainsArray (a, b) { if (a.length < b.length) { return false } for (const [index, element] of b.entries()) { if (a[index] !== element) { return false } } return true } /** * Compares a slice of a with a slice of b. * Slices start at startA and startB and have the same length * * @param {Array} a an Array * @param {Array} b an Array * @param {number} length slice length * @param {number} [startA=0] start index for the slice in a to compare * @param {number} [startB=0] start index for the slice in b to compare * @returns {boolean} true if slices are equal * @throws {'undefined length'} length is undefined * @throws {'start < 0'} when start is negative */ export function arraySlicesEqual (a, b, length, startA = 0, startB = 0) { if (length === null || length === undefined) { throw new Error('undefined length') } if (startA < 0 || startB < 0) { throw new Error('start < 0') } if (a.length < startA + length || b.length < startB + length) { // Array(s) too small for slicing with this start and length return false } for (let offset = 0; offset < length; offset++) { if (a[startA + offset] !== b[startB + offset]) { return false } } return true } /** * Finds an array in another array, only shallow comparison * * @param {Array} haystack array to search in * @param {Array} needle array to search for * @param {number} [startIndex=0] index from which to start searching * @returns {number} index or -1 when not found */ export function arrayIndexOf (haystack, needle, startIndex = 0) { if (needle.length === 0) { return -1 } for ( let index = startIndex; index < haystack.length - needle.length + 1; ++index ) { if (haystack[index] === needle[0]) { let found = true for (let offset = 1; offset < needle.length; ++offset) { if (haystack[index + offset] !== needle[offset]) { found = false break } } if (found) { return index } } } return -1 } /** * Returns the maximum numerical value from an array of arrays with arbitrary * depth and structure. * * @param {Array} array array * @returns {number} maximum value */ export function getArrayMax (array) { return d3.max(array.flat(Number.POSITIVE_INFINITY)) } /** * Normalizes by dividing all entries by the maximum. * Only for positive values! Assumes 0 to be the minimum. * Use normalizeNdArrayNegative for arbitrary numbers. * * @see normalizeNdArrayNegative * @param {Array} array nD array with arbitrary depth and structure * @returns {Array} normalized array */ export function normalizeNdArray (array) { const max = d3.max(array.flat(Number.POSITIVE_INFINITY)) const normalize = (array_, maxValue) => array_.map((d) => { return d.length !== undefined ? normalize(d, maxValue) : d / maxValue }) return normalize(array, max) } /** * Normalizes by dividing scaling all values to [0, 1] linearly. * * @todo move to musicvis-lib and test * @param {Array} array nD array with arbitrary depth and structure, containing * only numbers * @returns {Array} normalized array */ export function normalizeNdArrayNegative (array) { const extent = d3.extent(array.flat(Number.POSITIVE_INFINITY)) const scale = d3.scaleLinear().domain(extent) const normalize = (array_) => array_.map((d) => { return d.length !== undefined ? normalize(d) : scale(d) }) return normalize(array) } /** * Assumes same shape of matrices. * * @param {number[][]} matrixA a matrix * @param {number[][]} matrixB a matrix * @returns {number} Euclidean distance of the two matrices */ export function euclideanDistance (matrixA, matrixB) { const valuesA = matrixA.flat() const valuesB = matrixB.flat() const diffs = valuesA.map((d, i) => d - valuesB[i]) return Math.hypot(...diffs) } /** * Stringifies a 2D array / matrix for logging onto the console. * * @param {any[][]} matrix the matrix * @param {string} colSeparator column separator * @param {string} rowSeparator row separator * @param {Function} formatter formatting for each element * @returns {string} stringified matrix */ export function formatMatrix (matrix, colSeparator = ', ', rowSeparator = '\n', formatter) { if (!matrix || matrix.length === 0) { return '' } if (formatter) { matrix = matrix.map(row => row.map(value => formatter(value))) } return matrix.map(row => row.join(colSeparator)).join(rowSeparator) } /** * Returns the value in array that is closest to value. * Array MUST be sorted ascending. * * @param {Array} array array * @param {*} value value * @param {Function} accessor accessor * @returns {*} value in array closest to value */ export function binarySearch (array, value, accessor = d => d) { // Handle short arrays if (array.length <= 3) { let closest = null let diff = Number.POSITIVE_INFINITY for (const element of array) { const value_ = accessor(element) const diff2 = Math.abs(value - value_) if (diff2 < diff) { closest = element diff = diff2 } } return closest } // Split longer array in two for binary search const pivotPosition = Math.floor(array.length / 2) const pivotElement = array[pivotPosition] const pivotValue = accessor(pivotElement) if (value === pivotValue) { return pivotElement } if (value < pivotValue) { return binarySearch(array.slice(0, pivotPosition + 1), value, accessor) } if (value > pivotValue) { return binarySearch(array.slice(pivotPosition - 1), value, accessor) } } /** * Finds streaks of values in an array. * * @param {Array} values array * @param {Function} accessor value to compare * @param {Function} equality comparator for equality of two values * @returns {object[]} {startIndex, endIndex, length}[] * @example * const arr = [1, 1, 2, 3, 3, 3]; * const streaks = findStreaks(arr); */ export function findStreaks ( values, accessor = (d) => d, equality = (a, b) => a === b ) { let startIndex = 0 const result = [] let startValue = accessor(values[0]) for (const [index, value] of values.entries()) { const v = accessor(value) if (!equality(startValue, v)) { result.push({ startIndex, endIndex: index - 1, length: index - startIndex }) startIndex = index startValue = v } } // Finish last streak if (values.length > 0) { result.push({ startIndex, endIndex: values.length - 1, length: values.length - startIndex }) } return result } /** * For each element in a sequence, finds the lowest index where an equal element * occurs. * * @param {Array} sequence an Array * @param {Function} equals euqality function * @returns {number[]} result */ export function findRepeatedIndices (sequence, equals = (a, b) => a === b) { return sequence.map((element) => { for (const [index2, element2] of sequence.entries()) { if (equals(element, element2)) { return index2 } } return null }) }