From 1a9c6b9385c55632390c18427bdf26931da45df3 Mon Sep 17 00:00:00 2001 From: Evan Hahn <69474926+EvanHahn-Signal@users.noreply.github.com> Date: Wed, 31 Mar 2021 11:15:49 -0500 Subject: [PATCH] Clean up iterable utilities --- ts/sql/cleanDataForIpc.ts | 2 +- ts/test-both/util/isIterable_test.ts | 50 ------------------ ts/test-both/util/iterables_test.ts | 77 +++++++++++++++++++++++++++- ts/util/grapheme.ts | 10 ++-- ts/util/isIterable.ts | 9 ---- ts/util/iterables.ts | 25 +++++++++ 6 files changed, 105 insertions(+), 68 deletions(-) delete mode 100644 ts/test-both/util/isIterable_test.ts delete mode 100644 ts/util/isIterable.ts diff --git a/ts/sql/cleanDataForIpc.ts b/ts/sql/cleanDataForIpc.ts index 40c896d8522..c2ef37a3a47 100644 --- a/ts/sql/cleanDataForIpc.ts +++ b/ts/sql/cleanDataForIpc.ts @@ -3,7 +3,7 @@ import { isPlainObject } from 'lodash'; -import { isIterable } from '../util/isIterable'; +import { isIterable } from '../util/iterables'; /** * IPC arguments are serialized with the [structured clone algorithm][0], but we can only diff --git a/ts/test-both/util/isIterable_test.ts b/ts/test-both/util/isIterable_test.ts deleted file mode 100644 index 932d2f3af31..00000000000 --- a/ts/test-both/util/isIterable_test.ts +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { assert } from 'chai'; - -import { isIterable } from '../../util/isIterable'; - -describe('isIterable', () => { - it('returns false for non-iterables', () => { - assert.isFalse(isIterable(undefined)); - assert.isFalse(isIterable(null)); - assert.isFalse(isIterable(123)); - assert.isFalse(isIterable({ foo: 'bar' })); - assert.isFalse( - isIterable({ - length: 2, - '0': 'fake', - '1': 'array', - }) - ); - }); - - it('returns true for iterables', () => { - assert.isTrue(isIterable('strings are iterable')); - assert.isTrue(isIterable(['arrays too'])); - assert.isTrue(isIterable(new Set('and sets'))); - assert.isTrue(isIterable(new Map([['and', 'maps']]))); - assert.isTrue( - isIterable({ - [Symbol.iterator]() { - return { - next() { - return { - value: 'endless iterable', - done: false, - }; - }, - }; - }, - }) - ); - assert.isTrue( - isIterable( - (function* generators() { - yield 123; - })() - ) - ); - }); -}); diff --git a/ts/test-both/util/iterables_test.ts b/ts/test-both/util/iterables_test.ts index de47d0fabdb..b4c02e4193d 100644 --- a/ts/test-both/util/iterables_test.ts +++ b/ts/test-both/util/iterables_test.ts @@ -4,9 +4,84 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; -import { map, take } from '../../util/iterables'; +import { isIterable, size, map, take } from '../../util/iterables'; describe('iterable utilities', () => { + describe('isIterable', () => { + it('returns false for non-iterables', () => { + assert.isFalse(isIterable(undefined)); + assert.isFalse(isIterable(null)); + assert.isFalse(isIterable(123)); + assert.isFalse(isIterable({ foo: 'bar' })); + assert.isFalse( + isIterable({ + length: 2, + '0': 'fake', + '1': 'array', + }) + ); + }); + + it('returns true for iterables', () => { + assert.isTrue(isIterable('strings are iterable')); + assert.isTrue(isIterable(['arrays too'])); + assert.isTrue(isIterable(new Set('and sets'))); + assert.isTrue(isIterable(new Map([['and', 'maps']]))); + assert.isTrue( + isIterable({ + [Symbol.iterator]() { + return { + next() { + return { + value: 'endless iterable', + done: false, + }; + }, + }; + }, + }) + ); + assert.isTrue( + isIterable( + (function* generators() { + yield 123; + })() + ) + ); + }); + }); + + describe('size', () => { + it('returns the length of a string', () => { + assert.strictEqual(size(''), 0); + assert.strictEqual(size('hello world'), 11); + }); + + it('returns the length of an array', () => { + assert.strictEqual(size([]), 0); + assert.strictEqual(size(['hello', 'world']), 2); + }); + + it('returns the size of a set', () => { + assert.strictEqual(size(new Set()), 0); + assert.strictEqual(size(new Set([1, 2, 3])), 3); + }); + + it('returns the length (not byte length) of typed arrays', () => { + assert.strictEqual(size(new Uint8Array(3)), 3); + assert.strictEqual(size(new Uint32Array(3)), 3); + }); + + it('returns the size of arbitrary iterables', () => { + function* someNumbers() { + yield 3; + yield 6; + yield 9; + } + assert.strictEqual(size(someNumbers()), 3); + }); + }); + describe('map', () => { it('returns an empty iterable when passed an empty iterable', () => { const fn = sinon.fake(); diff --git a/ts/util/grapheme.ts b/ts/util/grapheme.ts index 46aac86bf69..25f933d3c88 100644 --- a/ts/util/grapheme.ts +++ b/ts/util/grapheme.ts @@ -1,13 +1,9 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import { size } from './iterables'; + export function count(str: string): number { const segments = new Intl.Segmenter().segment(str); - const iterator = segments[Symbol.iterator](); - - let result = -1; - for (let done = false; !done; result += 1) { - done = Boolean(iterator.next().done); - } - return result; + return size(segments); } diff --git a/ts/util/isIterable.ts b/ts/util/isIterable.ts deleted file mode 100644 index 04ff4dd7cd0..00000000000 --- a/ts/util/isIterable.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -export function isIterable(value: unknown): value is Iterable { - return ( - (typeof value === 'object' && value !== null && Symbol.iterator in value) || - typeof value === 'string' - ); -} diff --git a/ts/util/iterables.ts b/ts/util/iterables.ts index 379103d2719..2bd250be370 100644 --- a/ts/util/iterables.ts +++ b/ts/util/iterables.ts @@ -3,6 +3,31 @@ /* eslint-disable max-classes-per-file */ +export function isIterable(value: unknown): value is Iterable { + return ( + (typeof value === 'object' && value !== null && Symbol.iterator in value) || + typeof value === 'string' + ); +} + +export function size(iterable: Iterable): number { + // We check for common types as an optimization. + if (typeof iterable === 'string' || Array.isArray(iterable)) { + return iterable.length; + } + if (iterable instanceof Set || iterable instanceof Map) { + return iterable.size; + } + + const iterator = iterable[Symbol.iterator](); + + let result = -1; + for (let done = false; !done; result += 1) { + done = Boolean(iterator.next().done); + } + return result; +} + export function map( iterable: Iterable, fn: (value: T) => ResultT