Created
July 23, 2021 08:29
-
-
Save fostyfost/a7e9e55b0ebc444410730ef9488e90d2 to your computer and use it in GitHub Desktop.
Counter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | |
}) | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
}, | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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']) | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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