# core/selectors/rate.selector.ts

> 101 lines of code and 36 definitions.

Tree: Project Scripts
Language: typescript
Layer: application
Canonical: https://banes-lab.com/anatomy/scripts#file-scripts-core-selectors-rate-selector-ts
Source text: https://banes-lab.com/source/scripts/core/selectors/rate.selector.ts.txt

Listed in [core/selectors](https://banes-lab.com/api/source/scripts/core/selectors.md), after [core/selectors/manifest.selector.ts](https://banes-lab.com/source/scripts/core/selectors/manifest.selector.ts.md).

## Definitions

- `stringsOf` (lexical_declaration, line 36)
- `recordOf` (lexical_declaration, line 55)
- `numberOf` (lexical_declaration, line 41)
- `textAfter` (lexical_declaration, line 97)
- `parsedAt` (lexical_declaration, line 74)
- `statedUpdate` (lexical_declaration, line 110, exported)
- `OPEN` (lexical_declaration, line 6)
- `CLOSE` (lexical_declaration, line 7)
- `QUOTE` (lexical_declaration, line 8)
- `ESCAPE` (lexical_declaration, line 9)
- `TAG_END` (lexical_declaration, line 10)
- `TAG_START` (lexical_declaration, line 11)
- `DEPTH_STEP` (lexical_declaration, line 12)
- `objectEnd` (lexical_declaration, line 17)
- `depth` (lexical_declaration, line 18)
- `inString` (lexical_declaration, line 19)
- `char` (lexical_declaration, line 21)
- `list` (lexical_declaration, line 37)
- `rateOf` (lexical_declaration, line 45)
- `exact` (lexical_declaration, line 46)
- `low` (lexical_declaration, line 50)
- `high` (lexical_declaration, line 51)
- `rate` (lexical_declaration, line 59)
- `id` (lexical_declaration, line 60)
- `parsed` (lexical_declaration, line 76)
- `recordsIn` (lexical_declaration, line 83, exported)
- `byId` (lexical_declaration, line 84, exported)
- `start` (lexical_declaration, line 85, exported)
- `end` (lexical_declaration, line 87, exported)
- `record` (lexical_declaration, line 88, exported)
- `at` (lexical_declaration, line 98)
- `open` (lexical_declaration, line 99)
- `close` (lexical_declaration, line 100)
- `statedCount` (lexical_declaration, line 104, exported)
- `text` (lexical_declaration, line 105, exported)
- `count` (lexical_declaration, line 106, exported)

## Contained in

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

## Uses

- [configuration/strings/rate.strings.ts](https://banes-lab.com/source/scripts/configuration/strings/rate.strings.ts.md)
- [core/converters/viewport.converter.ts](https://banes-lab.com/source/scripts/core/converters/viewport.converter.ts.md)

## Linked from

- [configuration/strings](https://banes-lab.com/anatomy/scripts/folder-scripts-configuration-strings.md)
- [core/converters](https://banes-lab.com/anatomy/scripts/folder-scripts-core-converters.md)
- [core/selectors](https://banes-lab.com/anatomy/scripts/folder-scripts-core-selectors.md)

## Source

```typescript
import { COUNT_MARKER, RECORD_MARKER, UPDATE_MARKER } from "#configuration/constants/rate.constants";
import type { RateRecord } from "#types/rate.types";
import { isRecord } from "@banes-lab/build-scripts/core/selectors/base.selector.ts";
import { unreadableRecord } from "#configuration/strings/rate.strings";

const OPEN = "{";
const CLOSE = "}";
const QUOTE = '"';
const ESCAPE = "\\";
const TAG_END = ">";
const TAG_START = "<";
const DEPTH_STEP: ReadonlyMap<string, number> = new Map([
    [OPEN, 1],
    [CLOSE, -1],
]);

const objectEnd = function objectEnd(text: string, start: number): number {
    let depth = 0;
    let inString = false;
    for (let at = start; at < text.length; at += 1) {
        const char = text.charAt(at);
        if (inString) {
            at += char === ESCAPE ? 1 : 0;
            inString = char !== QUOTE;
        } else {
            inString = char === QUOTE;
            depth += DEPTH_STEP.get(char) ?? 0;
            if (depth === 0 && char === CLOSE) {
                return at;
            }
        }
    }
    return -1;
};

const stringsOf = function stringsOf(value: unknown, fallback: unknown): string[] {
    const list: unknown[] = Array.isArray(value) ? value : [fallback];
    return list.filter((item): item is string => typeof item === "string");
};

const numberOf = function numberOf(value: unknown): number | null {
    return typeof value === "number" && Number.isFinite(value) ? value : null;
};

const rateOf = function rateOf(raw: Record<string, unknown>): number | null {
    const exact = numberOf(raw["dailyRateEur"]);
    if (exact !== null) {
        return exact;
    }
    const low = numberOf(raw["dayRateMin"]);
    const high = numberOf(raw["dayRateMax"]) ?? low;
    return low === null || high === null ? null : (low + high) / 2;
};

const recordOf = function recordOf(raw: unknown): RateRecord | null {
    if (!isRecord(raw)) {
        return null;
    }
    const rate = rateOf(raw);
    const id = raw["externalId"];
    if (rate === null || typeof id !== "string") {
        return null;
    }
    return {
        country: typeof raw["country"] === "string" ? raw["country"] : "",
        createdAt: numberOf(raw["createdAtUtc"]) ?? 0,
        id,
        rate,
        seniorities: stringsOf(raw["seniorities"], raw["seniority"]),
        titles: stringsOf(raw["jobTitles"], raw["jobTitle"]),
    };
};

const parsedAt = function parsedAt(text: string, start: number, end: number): RateRecord | null {
    try {
        const parsed: unknown = JSON.parse(text.slice(start, end + 1));
        return recordOf(parsed);
    } catch (error: unknown) {
        throw new Error(unreadableRecord(start), { cause: error });
    }
};

export const recordsIn = function recordsIn(html: string): readonly RateRecord[] {
    const byId = new Map<string, RateRecord>();
    let start = html.indexOf(RECORD_MARKER);
    while (start !== -1) {
        const end = objectEnd(html, start);
        const record = end === -1 ? null : parsedAt(html, start, end);
        if (record !== null) {
            byId.set(record.id, record);
        }
        start = end === -1 ? -1 : html.indexOf(RECORD_MARKER, end + 1);
    }
    return [...byId.values()];
};

const textAfter = function textAfter(html: string, marker: string): string {
    const at = html.indexOf(marker);
    const open = at === -1 ? -1 : html.indexOf(TAG_END, at);
    const close = open === -1 ? -1 : html.indexOf(TAG_START, open);
    return close === -1 ? "" : html.slice(open + 1, close).trim();
};

export const statedCount = function statedCount(html: string): number | null {
    const text = textAfter(html, COUNT_MARKER);
    const count = Number(text);
    return text.length > 0 && Number.isInteger(count) ? count : null;
};

export const statedUpdate = function statedUpdate(html: string): string {
    return textAfter(html, UPDATE_MARKER);
};
```
