All files / src/ops/ordinal zipUniqueOrdinal.ts

100% Statements 30/30
100% Branches 9/9
100% Functions 1/1
100% Lines 30/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 761x 1x 1x 1x                                                                           1x 1x 8x 26x 26x 26x   26x 26x 26x 26x 26x 26x 26x 174x 174x 174x 144x 174x 174x 26x   26x 26x   1x   1x 200x 1x          
import {K} from '#Function'
import {pair, square} from '#Pair'
import {Order} from '#util'
import {Array, Option, pipe} from 'effect'
 
/**
 * Zip a list of options with the ordinal of the `somes` giving each unique
 * `some` its own ordinal.
 *
 * For example this list of string options:
 *
 * const options = [
 *   Option.some('I'),
 *   Option.none(),
 *   Option.some('II'),
 *   Option.some('II'),
 *   Option.some('III'),
 *   Option.none(),
 * ]
 *
 * When run through `zipUniqueOrdinal` with the ordinal starting at 5:
 *
 * const zipped = zipUniqueOrdinal(String.Equivalence)(options, 5)
 *
 * zipped = [
 *   Option.some(['I', 5]),
 *   Option.none(),
 *   Option.some(['II', 6]),
 *   Option.some(['II', 6]),
 *   Option.some(['III', 7]),
 *   Option.none(),
 * ]
 *
 * The input requirements:
 *
 * 1. The `A`s are grouped. This is illegal: I, II, I, III, but this is
 *    legal: I, I, II, III.
 * 2. No `None` values inside these groups, only between them.
 *
 * @category internal
 */
export const zipUniqueOrdinal =
  <A>(order: Order.Order<A>) =>
  (previousOrdinal: number) =>
  (xs: Options<A>): [options: Options<State<A>>, previousOrdinal: number] => {
    const equals = Order.orderToEqual(order)
    const [head, ...tail] = xs
 
    const scanned = Array.scan(
      tail,
      Option.match<A, State<A>>(head, {
        onNone: K([undefined, previousOrdinal] as const),
        onSome: pair.withSecond(previousOrdinal + 1),
      }),
      ([previous, i], current) =>
        Option.match<A, State<A>>(current, {
          onNone: K([undefined, i] as const),
          onSome: square.mapSecond(value =>
            previous !== undefined && equals(value, previous) ? i : i + 1,
          ),
        }),
    )
 
    return [asOptions(scanned), scanned.at(-1)?.at(-1) as number]
  }
 
const asOptions: <A>(
  xs: Array.NonEmptyArray<State<A>>,
) => Options<readonly [A, number]> = Array.map(([value, i]) =>
  pipe(value, Option.fromNullable, Option.map(pair.withSecond(i))),
)
 
type Options<A> = Array.NonEmptyArray<Option.Option<A>>
 
type State<A> = readonly [A | undefined, number]