Skip to content

Instantly share code, notes, and snippets.

@fostyfost
Created July 23, 2021 08:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fostyfost/a7e9e55b0ebc444410730ef9488e90d2 to your computer and use it in GitHub Desktop.
Save fostyfost/a7e9e55b0ebc444410730ef9488e90d2 to your computer and use it in GitHub Desktop.
Counter
import type { CountedItem} from './counter';
import { getCounter } from './counter'
describe('Counter tests', () => {
it('should work with functions', () => {
const noop1 = () => null
const noop2 = () => null
const counter = getCounter()
expect(counter.getCount(noop1)).toBe(0)
expect(counter.getItems()).toEqual([])
counter.add(noop1)
expect(counter.getCount(noop1)).toBe(1)
expect(counter.getItems()).toEqual([{ value: noop1, count: 1 }])
counter.add(noop2)
expect(counter.getCount(noop1)).toBe(1)
expect(counter.getCount(noop2)).toBe(1)
expect(counter.getItems()).toEqual([{ value: noop1, count: 1 }, { value: noop2, count: 1 }])
counter.add(noop2)
expect(counter.getCount(noop1)).toBe(1)
expect(counter.getCount(noop2)).toBe(2)
expect(counter.getItems()).toEqual([{ value: noop1, count: 1 }, { value: noop2, count: 2 }])
counter.remove(noop2)
expect(counter.getCount(noop1)).toBe(1)
expect(counter.getCount(noop2)).toBe(1)
expect(counter.getItems()).toEqual([{ value: noop1, count: 1 }, { value: noop2, count: 1 }])
counter.remove(noop2)
expect(counter.getCount(noop1)).toBe(1)
expect(counter.getCount(noop2)).toBe(0)
expect(counter.getItems()).toEqual([{ value: noop1, count: 1 }])
counter.remove(noop2)
expect(counter.getCount(noop1)).toBe(1)
expect(counter.getCount(noop2)).toBe(0)
expect(counter.getItems()).toEqual([{ value: noop1, count: 1 }])
counter.remove(noop1)
expect(counter.getCount(noop1)).toBe(0)
expect(counter.getCount(noop2)).toBe(0)
expect(counter.getItems()).toEqual([])
})
it('should work with strings and numbers', () => {
const data = {
value1: '1',
value2: 1,
value3: 'str',
value4: NaN,
value5: '0',
value6: 0,
}
const counter = getCounter<string | number>(Object.is)
const values = Object.values(data)
values.forEach(value => {
counter.add(value)
expect(counter.getCount(value)).toBe(1)
})
expect(counter.getItems()).toEqual(values.map(value => ({ value, count: 1 })))
values.forEach(value => {
counter.add(value)
expect(counter.getCount(value)).toBe(2)
})
expect(counter.getItems()).toEqual(values.map(value => ({ value, count: 2 })))
values.forEach(value => {
counter.remove(value)
expect(counter.getCount(value)).toBe(1)
})
expect(counter.getItems()).toEqual(values.map(value => ({ value, count: 1 })))
values.forEach(value => {
counter.remove(value)
expect(counter.getCount(value)).toBe(0)
})
expect(counter.getItems()).toEqual([])
values.forEach(value => {
counter.remove(value)
expect(counter.getCount(value)).toBe(0)
})
expect(counter.getItems()).toEqual([])
expect(counter.getCount(Number.NaN)).toBe(0)
expect(counter.getItems()).toEqual([])
expect(counter.getCount(NaN)).toBe(0)
expect(counter.getItems()).toEqual([])
counter.add(Number.NaN)
expect(counter.getCount(Number.NaN)).toBe(1)
expect(counter.getItems()).toEqual([{ value: Number.NaN, count: 1 }])
counter.add(NaN)
expect(counter.getCount(NaN)).toBe(2)
expect(counter.getItems()).toEqual([{ value: NaN, count: 2 }])
expect(counter.getItems()).toEqual([{ value: NaN, count: 2 }])
counter.remove(Number.NaN)
expect(counter.getCount(Number.NaN)).toBe(1)
expect(counter.getItems()).toEqual([{ value: Number.NaN, count: 1 }])
counter.remove(NaN)
expect(counter.getCount(NaN)).toBe(0)
expect(counter.getItems()).toEqual([])
})
it('should work with "retained" items', () => {
interface Item {
value: number
retained?: boolean
}
const values: Item[] = [
{
value: 1,
retained: true,
},
{
value: 2,
retained: false,
},
{
value: 3,
},
{
value: 4,
retained: true,
},
{
value: 5,
retained: true,
},
]
const checkIsRetained = (item: Item): boolean => !!item.retained
const counter = getCounter<Item>(undefined, checkIsRetained)
values.forEach(item => expect(counter.getCount(item)).toBe(0))
expect(counter.getItems()).toEqual([])
const mapper = (value: Item, expectedCount: number): CountedItem<Item> => {
return {
value,
count: checkIsRetained(value) ? Infinity : expectedCount
}
}
const filter = (item: CountedItem<Item>) => item.count > 0
values.forEach(item => {
counter.add(item)
expect(counter.getCount(item)).toBe(checkIsRetained(item) ? Infinity : 1)
})
expect(counter.getItems()).toEqual(values.map(value => mapper(value, 1)).filter(filter))
values.forEach(item => {
counter.add(item)
expect(counter.getCount(item)).toBe(checkIsRetained(item) ? Infinity : 2)
})
expect(counter.getItems()).toEqual(values.map(value => mapper(value, 2)).filter(filter))
values.forEach(item => {
counter.remove(item)
expect(counter.getCount(item)).toBe(checkIsRetained(item) ? Infinity : 1)
})
expect(counter.getItems()).toEqual(values.map(value => mapper(value, 1)).filter(filter))
values.forEach(item => {
counter.remove(item)
expect(counter.getCount(item)).toBe(checkIsRetained(item) ? Infinity : 0)
})
expect(counter.getItems()).toEqual(values.map(value => mapper(value, 0)).filter(filter))
values.forEach(item => {
counter.remove(item)
expect(counter.getCount(item)).toBe(checkIsRetained(item) ? Infinity : 0)
})
expect(counter.getItems()).toEqual(values.map(value => mapper(value, 0)).filter(filter))
})
})
export interface CountedItem<T> {
value: T
count: number
}
export interface Counter<T = unknown> {
getCount(item: T): number
getItems(): CountedItem<T>[]
add(item: T): void
remove(item: T): void
}
const defaultEqualityCheck = <T>(left: T, right: T): boolean => left === right
const defaultCheckIsRetained = (): boolean => false
export const getCounter = <T = unknown>(
equalityCheck: (left: T, right: T) => boolean = defaultEqualityCheck,
checkIsRetained: (value: T) => boolean = defaultCheckIsRetained,
): Counter<T> => {
const countedItems: CountedItem<T>[] = []
const getItem = (value: T): [CountedItem<T> | undefined, number] => {
const index = countedItems.findIndex(item => equalityCheck(item.value, value))
return [countedItems[index], index]
}
return {
getCount(value: T): number {
const [storeItem] = getItem(value)
return storeItem?.count || 0
},
getItems() {
return countedItems
},
add(value: T): void {
const [countedItem] = getItem(value)
if (countedItem && !checkIsRetained(countedItem.value)) {
countedItem.count += 1
} else if (!countedItem) {
countedItems.push({ value, count: checkIsRetained(value) ? Infinity : 1 })
}
},
remove(value: T): void {
const [countedItem, index] = getItem(value)
if (countedItem && !checkIsRetained(countedItem.value)) {
if (countedItem.count > 1) {
countedItem.count -= 1
} else {
countedItems.splice(index, 1)
}
}
},
}
}
import type { ItemManager } from './ref-counter'
import { getRefManager } from './ref-counter'
test('Ref manager tests', () => {
const items = new Set(['item1', 'item2'])
const manager: ItemManager<string> = {
getItems() {
return Array.from(items.keys())
},
add(values: string[]) {
if (values.length) {
values.forEach(value => items.add(value))
}
},
remove(values: string[]) {
values.forEach(value => items.delete(value))
},
}
const countedManager = getRefManager(manager)
expect(countedManager.getItems()).toEqual(['item1', 'item2'])
countedManager.add(['item1'])
expect(countedManager.getItems()).toEqual(['item1', 'item2'])
countedManager.remove(['item1'])
expect(countedManager.getItems()).toEqual(['item1', 'item2'])
countedManager.remove(['item1'])
expect(countedManager.getItems()).toEqual(['item2'])
countedManager.add(['item3'])
expect(countedManager.getItems()).toEqual(['item2', 'item3'])
countedManager.remove(['item3'])
expect(countedManager.getItems()).toEqual(['item2'])
})
import { getCounter } from './counter'
export interface ItemManager<T> {
getItems(): T[]
add(items: T[]): void
remove(items: T[]): void
}
/**
* Enhances the given items with ref counting for add remove purposes
*/
export const getRefManager = <Manager extends ItemManager<T>, T = unknown>(
manager: Manager,
equalityCheck?: (left: T, right: T) => boolean,
checkIsRetained?: (value: T) => boolean, // Decides if the item is retained even when the ref count reaches 0
): Manager => {
const counter = getCounter<T>(equalityCheck, checkIsRetained)
// Set initial ref counting
manager.getItems().forEach(counter.add)
return {
...manager,
add(items: T[]): void {
manager.add(items.filter(item => !counter.getCount(item)))
items.forEach(counter.add)
},
remove(items: T[]): void {
items.forEach(item => {
counter.remove(item)
if (!counter.getCount(item)) {
manager.remove([item])
}
})
},
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment