aboutsummaryrefslogtreecommitdiff
path: root/src/lib/utils
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/utils')
-rw-r--r--src/lib/utils/glyph_hash.test.ts68
-rw-r--r--src/lib/utils/glyph_hash.ts55
-rw-r--r--src/lib/utils/readable_time.test.ts83
-rw-r--r--src/lib/utils/readable_time.ts46
-rw-r--r--src/lib/utils/resolve_after.test.ts37
-rw-r--r--src/lib/utils/resolve_after.ts29
6 files changed, 318 insertions, 0 deletions
diff --git a/src/lib/utils/glyph_hash.test.ts b/src/lib/utils/glyph_hash.test.ts
new file mode 100644
index 0000000..5f57a2a
--- /dev/null
+++ b/src/lib/utils/glyph_hash.test.ts
@@ -0,0 +1,68 @@
+import { getGlyphHash } from './glyph_hash';
+import type { GlyphHash } from './glyph_hash';
+
+type TestState = {
+ glyphHash?: GlyphHash
+};
+
+
+describe('Glyph Hash utility', () => {
+
+ test('it throws an exception if the string is too short', () => {
+
+ expect(() => {
+
+ getGlyphHash('short');
+ }).toThrow();
+
+ expect(() => {
+
+ getGlyphHash('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
+ }).toThrow();
+
+ expect(() => {
+
+ getGlyphHash('abcdefghijklmnopqrstuvwxyzABCDEF');
+ }).not.toThrow();
+ });
+
+ test('it treats UUIDs with hyphens the same as those without', () => {
+
+ const uuidWithHyphens = 'f7722355-2285-46c0-a55f-3483a826f3a6';
+ const uuidWithoutHyphens = 'f7722355228546c0a55f3483a826f3a6';
+
+ expect(getGlyphHash(uuidWithHyphens)).toEqual(getGlyphHash(uuidWithoutHyphens));
+ });
+
+ describe('it generates four sets of glyphs and colors', () => {
+
+ const state: TestState = {};
+
+ beforeEach(() => {
+
+ state.glyphHash = getGlyphHash('f7722355-2285-46c0-a55f-3483a826f3a6');
+ });
+
+ test('there should be four glyph fragments', () => {
+
+ expect(state.glyphHash.length).toBe(4);
+ });
+
+ test('each fragment should have a single character glyph', () => {
+
+ for (const glyphHashFragment of state.glyphHash) {
+ expect(typeof glyphHashFragment.glyph).toBe('string');
+ expect(glyphHashFragment.glyph.length).toBe(1);
+ }
+ });
+
+ test('each fragment should have a hexadecimal color', () => {
+
+ for (const glyphHashFragment of state.glyphHash) {
+ expect(typeof glyphHashFragment.color).toBe('string');
+ expect(glyphHashFragment.color.length).toBe(7);
+ expect(glyphHashFragment.color).toEqual(expect.stringMatching(/#[0-9a-f]{6}/));
+ }
+ });
+ });
+});
diff --git a/src/lib/utils/glyph_hash.ts b/src/lib/utils/glyph_hash.ts
new file mode 100644
index 0000000..b704569
--- /dev/null
+++ b/src/lib/utils/glyph_hash.ts
@@ -0,0 +1,55 @@
+export type GlyphHash = GlyphHashFragment[];
+type GlyphHashFragment = {
+ glyph: string,
+ color: string
+};
+
+const internals = {
+ kDehyphenRegex: /[-]/g,
+ kSplitterRegex: /.{1,8}/g,
+ kGlyphs: [
+ '☽',
+ '☆',
+ '♢',
+ '♡',
+ '╱',
+ '╲',
+ '╳',
+ '〰',
+ '▷',
+ '⏊',
+ '〒',
+ '▢',
+ '◯',
+ '⏃',
+ '⏀',
+ '⏆'
+ ],
+ unexpectedUUIDLength: class UnexpectedUUIDLength extends Error {
+ name = 'UnexpectedUUIDLength';
+ message = 'The provided string was not a valid UUIDv4, please provide a 32 character long string'
+ }
+};
+
+// Return a glyph with color based on a 4 byte fragment of a UUIDv4
+const getGlyphHashFragment = function (uuidFragment: string): GlyphHashFragment {
+
+ const glyphIndex = parseInt(uuidFragment.substring(0, 2), 16) % 16;
+ return {
+ glyph: internals.kGlyphs[glyphIndex],
+ color: `#${uuidFragment.substring(2, 8)}`
+ };
+};
+
+// Return an array of glyphs based on a UUIDv4
+export const getGlyphHash = function (uuid: string): GlyphHash {
+
+ const dehyphenedUuid = uuid.replace(/[-]/g, '');
+
+ if (dehyphenedUuid.length !== 32) {
+ throw new internals.unexpectedUUIDLength();
+ }
+
+ const hashFragments = dehyphenedUuid.match(internals.kSplitterRegex);
+ return hashFragments.map(getGlyphHashFragment);
+};
diff --git a/src/lib/utils/readable_time.test.ts b/src/lib/utils/readable_time.test.ts
new file mode 100644
index 0000000..5d8ba27
--- /dev/null
+++ b/src/lib/utils/readable_time.test.ts
@@ -0,0 +1,83 @@
+import { readableTime } from './readable_time';
+
+describe('readableTime', () => {
+
+ test('it shows negative time as 0', () => {
+
+ const response = readableTime(-1000);
+
+ expect(response.count).toBe(0);
+ expect(response.label).toContain('seconds');
+ });
+
+ test('uses seconds as the smallest unit', () => {
+
+ const response = readableTime(10);
+
+ expect(response.count).toBe(0);
+ expect(response.label).toContain('seconds');
+ });
+
+ test('correctly divides miliseconds into seconds', () => {
+
+ const response = readableTime(4000);
+
+ expect(response.count).toBe(4);
+ });
+
+ test('uses seconds if the time is < 1 minute', () => {
+
+ const response = readableTime(59 * 1000);
+
+ expect(response.label).toContain('seconds');
+ });
+
+ test('correctly divides miliseconds into minutes', () => {
+
+ const response = readableTime(2 * 60 * 1000);
+
+ expect(response.count).toBe(2);
+ });
+
+ test('uses minutes if the time is < 1 hour', () => {
+
+ const response = readableTime(59 * 60 * 1000);
+
+ expect(response.label).toContain('minutes');
+ });
+
+ test('correctly divides miliseconds into hours', () => {
+
+ const response = readableTime(2 * 60 * 60 * 1000);
+
+ expect(response.count).toBe(2);
+ });
+
+ test('uses hours if the time is < 1 days', () => {
+
+ const response = readableTime(23 * 60 * 60 * 1000);
+
+ expect(response.label).toContain('hours');
+ });
+
+ test('correctly divides miliseconds into days', () => {
+
+ const response = readableTime(2 * 24 * 60 * 60 * 1000);
+
+ expect(response.count).toBe(2);
+ });
+
+ test('uses days if the time is >= 1 day', () => {
+
+ const response = readableTime(364 * 24 * 60 * 60 * 1000);
+
+ expect(response.label).toContain('days');
+ });
+
+ test('uses days as the maximum unit', () => {
+
+ const response = readableTime(Number.MAX_VALUE);
+
+ expect(response.label).toContain('days');
+ });
+});
diff --git a/src/lib/utils/readable_time.ts b/src/lib/utils/readable_time.ts
new file mode 100644
index 0000000..86ba044
--- /dev/null
+++ b/src/lib/utils/readable_time.ts
@@ -0,0 +1,46 @@
+type DateMagnitude = 'day' | 'hour' | 'minute' | 'second';
+
+type ReadableTime = {
+ count: number,
+ label: string
+};
+
+
+const internals = {
+ magnitudes: {
+ day: 86400000,
+ hour: 3600000,
+ minute: 60000,
+ second: 1000
+ },
+ labels: {
+ day: 'time.days',
+ hour: 'time.hours',
+ minute: 'time.minutes',
+ second: 'time.seconds'
+ },
+
+ makeTimeReadable(time: number, magnitude: DateMagnitude): ReadableTime {
+
+ return {
+ count: Math.floor(time / internals.magnitudes[magnitude]),
+ label: internals.labels[magnitude]
+ };
+ }
+};
+
+export const readableTime = function readableTime(time: number): ReadableTime {
+
+ switch (true) {
+ case time >= internals.magnitudes.day:
+ return internals.makeTimeReadable(time, 'day');
+ case time >= internals.magnitudes.hour:
+ return internals.makeTimeReadable(time, 'hour');
+ case time >= internals.magnitudes.minute:
+ return internals.makeTimeReadable(time, 'minute');
+ case time < 0:
+ return internals.makeTimeReadable(0, 'second');
+ default:
+ return internals.makeTimeReadable(time, 'second');
+ }
+};
diff --git a/src/lib/utils/resolve_after.test.ts b/src/lib/utils/resolve_after.test.ts
new file mode 100644
index 0000000..7e0cc3c
--- /dev/null
+++ b/src/lib/utils/resolve_after.test.ts
@@ -0,0 +1,37 @@
+import { resolveAfter } from './resolve_after';
+
+describe('Resolve After', () => {
+
+ test('it should throw if given 0', () => {
+
+ expect(() => {
+
+ resolveAfter(0);
+ }).toThrow();
+ });
+
+ test('it should throw if given a negative number', () => {
+
+ expect(() => {
+
+ resolveAfter(-1);
+ }).toThrow();
+ });
+
+ test('it should resolve after the specified number of times', () => {
+
+ expect(() => {
+
+ const { counter, promise: resolveAfterThree } = resolveAfter(3);
+ let resolved = false;
+
+ resolveAfterThree.then(() => (resolved = true));
+ counter();
+ expect(resolved).toBe(false);
+ counter();
+ expect(resolved).toBe(false);
+ counter();
+ expect(resolved).toBe(true);
+ }).toThrow();
+ });
+});
diff --git a/src/lib/utils/resolve_after.ts b/src/lib/utils/resolve_after.ts
new file mode 100644
index 0000000..95a477e
--- /dev/null
+++ b/src/lib/utils/resolve_after.ts
@@ -0,0 +1,29 @@
+export type ResolveAfterPromise = {
+ counter: () => void,
+ promise: Promise<void>
+};
+
+export const resolveAfter = function (timesUntilResolve: number): ResolveAfterPromise {
+
+ let counter = null;
+ let currentValue = 0;
+
+ if (timesUntilResolve <= 0) {
+ throw new Error('Resolve after requires a positive integer');
+ }
+
+ const promise: Promise<void> = new Promise((resolvePromise) => {
+
+ counter = () => {
+
+ if (++currentValue === timesUntilResolve) {
+ resolvePromise();
+ }
+ };
+ });
+
+ return {
+ counter,
+ promise
+ };
+};