From f29e0b0c7c496c7e625e99a185b40e68793b62fd Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Wed, 24 Jun 2026 13:48:50 +0300 Subject: [PATCH] feat(Pairing): add nextFocalId cycle math and expose slice API --- src/entities/Pairing/domain/index.ts | 3 ++ .../domain/nextFocalId/nextFocalId.test.ts | 32 +++++++++++++++++++ .../Pairing/domain/nextFocalId/nextFocalId.ts | 21 ++++++++++++ src/entities/Pairing/index.ts | 9 ++++++ 4 files changed, 65 insertions(+) create mode 100644 src/entities/Pairing/domain/index.ts create mode 100644 src/entities/Pairing/domain/nextFocalId/nextFocalId.test.ts create mode 100644 src/entities/Pairing/domain/nextFocalId/nextFocalId.ts create mode 100644 src/entities/Pairing/index.ts diff --git a/src/entities/Pairing/domain/index.ts b/src/entities/Pairing/domain/index.ts new file mode 100644 index 0000000..f13963d --- /dev/null +++ b/src/entities/Pairing/domain/index.ts @@ -0,0 +1,3 @@ +export { comboKey } from './comboKey/comboKey'; +export { createPairing } from './createPairing/createPairing'; +export { nextFocalId } from './nextFocalId/nextFocalId'; diff --git a/src/entities/Pairing/domain/nextFocalId/nextFocalId.test.ts b/src/entities/Pairing/domain/nextFocalId/nextFocalId.test.ts new file mode 100644 index 0000000..9a58119 --- /dev/null +++ b/src/entities/Pairing/domain/nextFocalId/nextFocalId.test.ts @@ -0,0 +1,32 @@ +import { + describe, + expect, + it, +} from 'vitest'; +import { nextFocalId } from './nextFocalId'; + +const ids = ['a', 'b', 'c']; + +describe('nextFocalId', () => { + it('steps forward', () => { + expect(nextFocalId(ids, 'a', 1)).toBe('b'); + }); + it('steps backward', () => { + expect(nextFocalId(ids, 'b', -1)).toBe('a'); + }); + it('wraps forward at the end', () => { + expect(nextFocalId(ids, 'c', 1)).toBe('a'); + }); + it('wraps backward at the start', () => { + expect(nextFocalId(ids, 'a', -1)).toBe('c'); + }); + it('returns the only id when list has one', () => { + expect(nextFocalId(['solo'], 'solo', 1)).toBe('solo'); + }); + it('returns current when focal id is absent', () => { + expect(nextFocalId(ids, 'missing', 1)).toBe('missing'); + }); + it('returns null for an empty list', () => { + expect(nextFocalId([], 'x', 1)).toBeNull(); + }); +}); diff --git a/src/entities/Pairing/domain/nextFocalId/nextFocalId.ts b/src/entities/Pairing/domain/nextFocalId/nextFocalId.ts new file mode 100644 index 0000000..558952a --- /dev/null +++ b/src/entities/Pairing/domain/nextFocalId/nextFocalId.ts @@ -0,0 +1,21 @@ +/** + * The id one step from `currentId` in board order, wrapping at both ends. + * + * @param orderedIds - Pairing ids in board order. + * @param currentId - The currently focal id to step from. + * @param direction - +1 for next, -1 for previous. + * @returns The neighbouring id (wrapped), `currentId` unchanged if it isn't in + * the list, or null for an empty list. + */ +export function nextFocalId(orderedIds: string[], currentId: string, direction: 1 | -1): string | null { + if (orderedIds.length === 0) { + return null; + } + const i = orderedIds.indexOf(currentId); + if (i === -1) { + return currentId; + } + const len = orderedIds.length; + const next = (i + direction + len) % len; + return orderedIds[next]; +} diff --git a/src/entities/Pairing/index.ts b/src/entities/Pairing/index.ts new file mode 100644 index 0000000..a570672 --- /dev/null +++ b/src/entities/Pairing/index.ts @@ -0,0 +1,9 @@ +export { + comboKey, + createPairing, + nextFocalId, +} from './domain'; +export type { + Pairing, + Role, +} from './model/types';