Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/aggregate-collection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { test, expect } from 'vitest'
import { AggregateCollection } from './aggregate-collection.js'

test('aggregates correctly', () => {
const fixture = new AggregateCollection()
const fixture = new AggregateCollection(true)
fixture.push(1)
fixture.push(2)
fixture.push(25)
Expand Down
98 changes: 45 additions & 53 deletions src/aggregate-collection.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,44 @@
/**
* Find the mode (most occurring value) in an array of Numbers
* Takes the mean/average of multiple values if multiple values occur the same amount of times.
*
* @see https://github.com/angus-c/just/blob/684af9ca0c7808bc78543ec89379b1fdfce502b1/packages/array-mode/index.js
* @param arr - Array to find the mode value for
* @returns mode - The `mode` value of `arr`
*/
function Mode(arr: unknown[]): number {
let frequencies = new Map()
let maxOccurrences = -1
let maxOccurenceCount = 0
let sum = 0
let len = arr.length

for (let i = 0; i < len; i++) {
let element = arr[i]
let updatedCount = (frequencies.get(element) || 0) + 1
frequencies.set(element, updatedCount)

if (updatedCount > maxOccurrences) {
maxOccurrences = updatedCount
maxOccurenceCount = 0
sum = 0
}

if (updatedCount >= maxOccurrences) {
maxOccurenceCount++
// @ts-expect-error TODO: fix this
sum += element
}
}

return sum / maxOccurenceCount
}

export class AggregateCollection {
#items: number[]
#min: number
#max: number
#sum: number
#count: number
#frequencies: Map<number, number>
#items: number[] | null

constructor() {
this.#items = []
constructor(samples = false) {
this.#min = Infinity
this.#max = -Infinity
this.#sum = 0
this.#count = 0
this.#frequencies = new Map()
this.#items = samples ? [] : null
}

/**
* Add a new Integer at the end of this AggregateCollection
* @param item - The item to add
*/
push(item: number) {
this.#items.push(item)
this.#sum += item
this.#count++
if (item < this.#min) this.#min = item
if (item > this.#max) this.#max = item

let freq = (this.#frequencies.get(item) || 0) + 1
this.#frequencies.set(item, freq)

if (this.#items !== null) {
this.#items.push(item)
}
}

size() {
return this.#items.length
return this.#count
}

aggregate() {
let len = this.#items.length
let len = this.#count

if (len === 0) {
return {
Expand All @@ -70,25 +51,36 @@ export class AggregateCollection {
}
}

// TODO: can we avoid this sort()? It's slow
let sorted = this.#items.slice().sort((a, b) => a - b)
let min = sorted[0]!
let max = sorted[len - 1]!
// Find max frequency for mode calculation — O(k) where k = unique values
let maxFreq = 0
for (let freq of this.#frequencies.values()) {
if (freq > maxFreq) maxFreq = freq
}

// Average of all values with max frequency (matches prior behavior)
let modeSum = 0
let modeCount = 0
for (let [val, freq] of this.#frequencies) {
if (freq === maxFreq) {
modeSum += val
modeCount++
}
}

let mode = Mode(sorted)
let sum = this.#sum
let min = this.#min
let max = this.#max

return {
min,
max,
mean: sum / len,
mode,
mean: this.#sum / len,
mode: modeSum / modeCount,
range: max - min,
sum: sum,
sum: this.#sum,
}
}

toArray() {
return this.#items
toArray(): number[] {
return this.#items ?? []
}
}
9 changes: 5 additions & 4 deletions src/atrules/atrules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,10 @@ test('finds @font-face (with locations)', () => {
src: local('Input Mono') url("https://url-to-input-mono.woff");
}
}`
const actual = analyze(fixture, {
useLocations: true,
}).atrules.fontface.uniqueWithLocations
const result = analyze(fixture, {
locations: true,
})
const actual = result.locations['atrules.fontface']
const expected = {
5: [
{
Expand Down Expand Up @@ -1047,7 +1048,7 @@ test('tracks nesting depth', () => {
}
}
`
const actual = analyze(fixture).atrules.nesting
const actual = analyze(fixture, { samples: true }).atrules.nesting
const expected = {
min: 0,
max: 1,
Expand Down
10 changes: 6 additions & 4 deletions src/collection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,16 @@ test('count with useLocations=true', () => {
collection.p('a', loc)
collection.p('a', loc)

let pos = { offset: 1, length: 1, line: 1, column: 1 }
let count = collection.c()
expect(count).toEqual({
total: 2,
totalUnique: 1,
unique: {},
unique: { a: 2 },
uniquenessRatio: 0.5,
uniqueWithLocations: { a: [pos, pos] },
})
expectTypeOf(count['uniqueWithLocations']).toMatchObjectType<UniqueWithLocations>()
expectTypeOf(count['uniqueWithLocations']).toBeUndefined()

let locs = collection.locs()
let pos = { offset: 1, length: 1, line: 1, column: 1 }
expect(locs).toEqual({ a: [pos, pos] })
})
82 changes: 36 additions & 46 deletions src/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,39 @@ export type Location = {

export type UniqueWithLocations = Record<string, Location[]>

export type CollectionCount<WithLocations extends boolean = false> = {
export type CollectionCount = {
total: number
totalUnique: number
unique: Record<string, number>
uniquenessRatio: number
} & (WithLocations extends true ? { uniqueWithLocations: UniqueWithLocations } : { uniqueWithLocations?: undefined })
}

export class Collection<UseLocations extends boolean = false> {
export class Collection {
#items: Map<string | number, number[]>
#total: number
#nodes: number[] = []
#useLocations: UseLocations
#nodes: number[] | null
#useLocations: boolean

constructor(useLocations: UseLocations = false as UseLocations) {
constructor(useLocations = false) {
this.#items = new Map()
this.#total = 0

if (useLocations) {
this.#nodes = []
}

this.#nodes = useLocations ? [] : null
this.#useLocations = useLocations
}

p(item: string | number, node_location: Location) {
let index = this.#total

if (this.#useLocations) {
if (this.#nodes !== null) {
let position = index * 4

this.#nodes[position] = node_location.line
this.#nodes[position + 1] = node_location.column
this.#nodes[position + 2] = node_location.offset
this.#nodes[position + 3] = node_location.length
}

if (this.#items.has(item)) {
let list = this.#items.get(item)!
list.push(index)
this.#items.get(item)!.push(index)
this.#total++
return
}
Expand All @@ -58,49 +52,45 @@ export class Collection<UseLocations extends boolean = false> {
return this.#total
}

c(): CollectionCount<UseLocations> {
let uniqueWithLocations: Map<string | number, Location[]> = new Map()
/** Returns counts only — never location data */
c(): CollectionCount {
let unique: Record<string, number> = {}
let useLocations = this.#useLocations
let items = this.#items
let _nodes = this.#nodes
let size = items.size

items.forEach((list, key) => {
if (useLocations) {
let nodes = list.map(function (index) {
let position = index * 4
return {
line: _nodes[position]!,
column: _nodes[position + 1]!,
offset: _nodes[position + 2]!,
length: _nodes[position + 3]!,
}
})
uniqueWithLocations.set(key, nodes)
} else {
unique[key] = list.length
}
unique[key] = list.length
})

let total = this.#total

if (useLocations) {
return {
total,
totalUnique: size,
unique,
uniquenessRatio: total === 0 ? 0 : size / total,
uniqueWithLocations: Object.fromEntries(uniqueWithLocations),
} as unknown as CollectionCount<UseLocations>
}
let size = items.size

return {
total,
totalUnique: size,
unique,
uniquenessRatio: total === 0 ? 0 : size / total,
uniqueWithLocations: undefined,
} as unknown as CollectionCount<UseLocations>
}
}

/** Returns location data per unique value, or undefined when not tracking locations */
locs(): UniqueWithLocations | undefined {
if (!this.#useLocations || this.#nodes === null) return undefined

let result: UniqueWithLocations = {}
let _nodes = this.#nodes

this.#items.forEach((list, key) => {
result[String(key)] = list.map(function (index) {
let position = index * 4
return {
line: _nodes[position]!,
column: _nodes[position + 1]!,
offset: _nodes[position + 2]!,
length: _nodes[position + 3]!,
}
})
})

return result
}
}
30 changes: 23 additions & 7 deletions src/context-collection.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Collection, type CollectionCount, type Location } from './collection.js'
import { Collection, type CollectionCount, type Location, type UniqueWithLocations } from './collection.js'

export class ContextCollection<UseLocations extends boolean = false> {
#list: Collection<UseLocations>
#contexts: Map<string, Collection<UseLocations>>
#useLocations: UseLocations
export class ContextCollection {
#list: Collection
#contexts: Map<string, Collection>
#useLocations: boolean

constructor(useLocations: UseLocations) {
constructor(useLocations = false) {
this.#list = new Collection(useLocations)
this.#contexts = new Map()
this.#useLocations = useLocations
Expand All @@ -28,7 +28,7 @@ export class ContextCollection<UseLocations extends boolean = false> {
}

count() {
let itemsPerContext: Map<string, CollectionCount<UseLocations>> = new Map()
let itemsPerContext: Map<string, CollectionCount> = new Map()

for (let [context, value] of this.#contexts.entries()) {
itemsPerContext.set(context, value.c())
Expand All @@ -38,4 +38,20 @@ export class ContextCollection<UseLocations extends boolean = false> {
itemsPerContext: Object.fromEntries(itemsPerContext),
})
}

/** Returns location data for the top-level list, or undefined when not tracking locations */
locs(): UniqueWithLocations | undefined {
return this.#list.locs()
}

/** Returns location data per context, or undefined when not tracking locations */
locsPerContext(): Record<string, UniqueWithLocations> | undefined {
if (!this.#useLocations) return undefined

let result: Record<string, UniqueWithLocations> = {}
for (let [context, collection] of this.#contexts.entries()) {
result[context] = collection.locs() ?? {}
}
return result
}
}
2 changes: 1 addition & 1 deletion src/declarations/declarations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ test('tracks nesting depth', () => {
}
}
`
const actual = analyze(fixture).declarations.nesting
const actual = analyze(fixture, { samples: true }).declarations.nesting
const expected = {
min: 0,
max: 2,
Expand Down
Loading
Loading