# core/analyzers/representation.distribution.analyzer.ts

> 98 lines of code and 26 definitions.

Tree: GovLab Patterns
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/patterns#file-patterns-core-analyzers-representation-distribution-analyzer-ts
Source text: https://banes-lab.com/source/patterns/core/analyzers/representation.distribution.analyzer.ts.txt

Listed in [core/analyzers](https://banes-lab.com/api/source/patterns/core/analyzers.md), after [core/analyzers/information.analyzer.ts](https://banes-lab.com/source/patterns/core/analyzers/information.analyzer.ts.md) and before [core/analyzers/representation.graph.analyzer.ts](https://banes-lab.com/source/patterns/core/analyzers/representation.graph.analyzer.ts.md).

## Definitions

- `temporalOf` (lexical_declaration, line 48, exported)
- `witnessesOf` (lexical_declaration, line 61, exported)
- `overdueOf` (lexical_declaration, line 68, exported)
- `driftOf` (lexical_declaration, line 94, exported)
- `bucketMonths` (lexical_declaration, line 34)
- `temperaturesOf` (lexical_declaration, line 79, exported)
- `HALF` (lexical_declaration, line 5)
- `YEAR_END` (lexical_declaration, line 6)
- `MONTH_START` (lexical_declaration, line 7)
- `MONTH_END` (lexical_declaration, line 8)
- `ISO_LENGTH` (lexical_declaration, line 9)
- `DATE_SEPARATOR` (lexical_declaration, line 10)
- `DIGITS` (lexical_declaration, line 11)
- `MonthBuckets` (interface_declaration, line 13)
- `byMagnitudeDesc` (lexical_declaration, line 18)
- `isoMonth` (lexical_declaration, line 22)
- `i` (lexical_declaration, line 26)
- `byMonth` (lexical_declaration, line 35)
- `moments` (lexical_declaration, line 36)
- `month` (lexical_declaration, line 38)
- `bucketed` (lexical_declaration, line 49, exported)
- `sorted` (lexical_declaration, line 53, exported)
- `recent` (lexical_declaration, line 84, exported)
- `{ indexSum, total, limit }` (lexical_declaration, line 98, exported)
- `midpoint` (lexical_declaration, line 102, exported)
- `scored` (lexical_declaration, line 103, exported)

## Contained in

- [core/analyzers](https://banes-lab.com/anatomy/patterns/folder-patterns-core-analyzers.md)

## Uses

- [core/counters/base.counter.ts](https://banes-lab.com/source/patterns/core/counters/base.counter.ts.md)

## Used by

- [core/aggregators/representation.distribution.aggregator.ts](https://banes-lab.com/source/patterns/core/aggregators/representation.distribution.aggregator.ts.md)

## Linked from

- [core/aggregators](https://banes-lab.com/anatomy/patterns/folder-patterns-core-aggregators.md)
- [core/counters](https://banes-lab.com/anatomy/patterns/folder-patterns-core-counters.md)

## Source

```typescript
import type { DriftContext, Temporal } from "#types/representation.types";
import { byCountDesc } from "#core/selectors/counter.selector";
import { increment } from "#core/counters/base.counter";

const HALF = 0.5;
const YEAR_END = 4;
const MONTH_START = 5;
const MONTH_END = 7;
const ISO_LENGTH = 10;
const DATE_SEPARATOR = "-";
const DIGITS: ReadonlySet<string> = new Set(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]);

interface MonthBuckets {
    byMonth: Map<number, number>;
    moments: string[];
}

const byMagnitudeDesc = function byMagnitudeDesc(a: readonly [string, number], b: readonly [string, number]): number {
    return Math.abs(b[1]) - Math.abs(a[1]) || a[0].localeCompare(b[0]);
};

const isoMonth = function isoMonth(value: string): number | null {
    if (value.length !== ISO_LENGTH || value[YEAR_END] !== DATE_SEPARATOR || value[MONTH_END] !== DATE_SEPARATOR) {
        return null;
    }
    for (let i = 0; i < value.length; i += 1) {
        if (i !== YEAR_END && i !== MONTH_END && !DIGITS.has(value[i] ?? "")) {
            return null;
        }
    }
    return Number(value.slice(MONTH_START, MONTH_END));
};

const bucketMonths = function bucketMonths(counts: ReadonlyMap<string, number>): MonthBuckets | null {
    const byMonth = new Map<number, number>();
    const moments: string[] = [];
    for (const [value, count] of counts) {
        const month = isoMonth(value);
        if (month === null) {
            return null;
        }
        increment(byMonth, month, count);
        moments.push(value);
    }
    return { byMonth, moments };
};

export const temporalOf = function temporalOf(counts: ReadonlyMap<string, number>): Temporal | null {
    const bucketed = bucketMonths(counts);
    if (bucketed === null || bucketed.moments.length === 0) {
        return null;
    }
    const sorted = bucketed.moments.toSorted((a, b) => a.localeCompare(b));
    return {
        byMonth: [...bucketed.byMonth.entries()].sort((a, b) => a[0] - b[0]),
        first: sorted[0] ?? "",
        last: sorted.at(-1) ?? "",
    };
};

export const witnessesOf = function witnessesOf(
    top: readonly [string, number][],
    lastSeen: ReadonlyMap<string, number>,
): [string, number][] {
    return top.map(([value]): [string, number] => [value, lastSeen.get(value) ?? 0]);
};

export const overdueOf = function overdueOf(
    lastSeen: ReadonlyMap<string, number>,
    total: number,
    limit: number,
): [string, number][] {
    return [...lastSeen.entries()]
        .map(([value, index]): [string, number] => [value, total - 1 - index])
        .sort(byCountDesc)
        .slice(0, limit);
};

export const temperaturesOf = function temperaturesOf(
    counts: ReadonlyMap<string, number>,
    window: readonly string[],
    total: number,
): [string, number][] {
    const recent = new Map<string, number>();
    for (const value of window) {
        increment(recent, value);
    }
    return [...counts.entries()].map(([value, count]): [string, number] => [
        value,
        (recent.get(value) ?? 0) - (count / total) * window.length,
    ]);
};

export const driftOf = function driftOf(
    counts: ReadonlyMap<string, number>,
    context: DriftContext,
): [string, number][] {
    const { indexSum, total, limit } = context;
    if (total <= 1) {
        return [];
    }
    const midpoint = (total - 1) * HALF;
    const scored = [...counts.keys()].map((value): [string, number] => [
        value,
        ((indexSum.get(value) ?? 0) / (counts.get(value) ?? 1) - midpoint) / (total - 1),
    ]);
    return scored.toSorted(byMagnitudeDesc).slice(0, limit);
};
```
