# core/classifiers/concern.classifier.ts

> 69 lines of code and 22 definitions.

Tree: GovLab Quality
Language: typescript
Layer: application
Canonical: https://banes-lab.com/anatomy/quality#file-quality-core-classifiers-concern-classifier-ts
Source text: https://banes-lab.com/source/quality/core/classifiers/concern.classifier.ts.txt

Listed in [core/classifiers](https://banes-lab.com/api/source/quality/core/classifiers.md), after [core/classifiers/concept.classifier.ts](https://banes-lab.com/source/quality/core/classifiers/concept.classifier.ts.md) and before [core/classifiers/invocation.classifier.ts](https://banes-lab.com/source/quality/core/classifiers/invocation.classifier.ts.md).

## Definitions

- `labelOf` (lexical_declaration, line 17, exported)
- `isCode` (lexical_declaration, line 35)
- `concernOf` (lexical_declaration, line 66, exported)
- `normalizeToken` (lexical_declaration, line 43)
- `meaningful` (lexical_declaration, line 52)
- `STEM_MIN` (lexical_declaration, line 7)
- `MAX_CODE_PREFIX` (lexical_declaration, line 8)
- `MIN_WORD_LEN` (lexical_declaration, line 9)
- `DESC_CAP` (lexical_declaration, line 10)
- `MIN_SIG` (lexical_declaration, line 11)
- `FALLBACK_SLICE` (lexical_declaration, line 12)
- `IES_DROP` (lexical_declaration, line 13)
- `ES_DROP` (lexical_declaration, line 14)
- `FALLBACK_PREFIX` (lexical_declaration, line 15)
- `stem` (lexical_declaration, line 21)
- `simplePlural` (lexical_declaration, line 31)
- `i` (lexical_declaration, line 36)
- `synonym` (lexical_declaration, line 47)
- `word` (lexical_declaration, line 55)
- `fromLabel` (lexical_declaration, line 67, exported)
- `fromDescription` (lexical_declaration, line 68, exported)
- `sig` (lexical_declaration, line 69, exported)

## Contained in

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

## Uses

- [core/parsers/word.parser.ts](https://banes-lab.com/source/quality/core/parsers/word.parser.ts.md)

## Linked from

- [core/classifiers](https://banes-lab.com/anatomy/quality/folder-quality-core-classifiers.md)
- [core/parsers](https://banes-lab.com/anatomy/quality/folder-quality-core-parsers.md)

## Source

```typescript
import { ES_PLURAL_SUFFIXES, STOP_WORDS, SYNONYMS, S_KEEP_SUFFIXES } from "#configuration/constants/word.constants";
import { everyChar, isDigit } from "@govlab/constants";
import { labelOfRule, tokenizeWords } from "#core/parsers/word.parser";
import type { CatalogRule } from "#types/catalog.types";
import { stringField } from "#core/selectors/record.selector";

const STEM_MIN = 4;
const MAX_CODE_PREFIX = 4;
const MIN_WORD_LEN = 3;
const DESC_CAP = 6;
const MIN_SIG = 2;
const FALLBACK_SLICE = 50;
const IES_DROP = 3;
const ES_DROP = 2;
const FALLBACK_PREFIX = "x:";

export const labelOf = function labelOf(rule: CatalogRule): string {
    return labelOfRule(stringField(rule, "name") || rule.ruleId);
};

const stem = function stem(word: string): string {
    if (word.length <= STEM_MIN) {
        return word;
    }
    if (word.endsWith("ies")) {
        return `${word.slice(0, -IES_DROP)}y`;
    }
    if (ES_PLURAL_SUFFIXES.some((suffix) => word.endsWith(suffix))) {
        return word.slice(0, -ES_DROP);
    }
    const simplePlural = word.endsWith("s") && !S_KEEP_SUFFIXES.some((suffix) => word.endsWith(suffix));
    return simplePlural ? word.slice(0, -1) : word;
};

const isCode = function isCode(token: string): boolean {
    let i = 0;
    while (i < token.length && !isDigit(token[i] ?? "")) {
        i += 1;
    }
    return i < token.length && i <= MAX_CODE_PREFIX && everyChar(token.slice(i), isDigit);
};

const normalizeToken = function normalizeToken(token: string): string | null {
    if (token.length < MIN_WORD_LEN || isCode(token) || STOP_WORDS.has(token)) {
        return null;
    }
    const synonym = SYNONYMS.get(token) ?? token;
    const word = stem(SYNONYMS.get(synonym) ?? synonym);
    return STOP_WORDS.has(word) || word.length < MIN_WORD_LEN ? null : word;
};

const meaningful = function meaningful(text: string, cap?: number): string[] {
    const sig: string[] = [];
    for (const token of tokenizeWords(text)) {
        const word = normalizeToken(token);
        if (word !== null && !sig.includes(word)) {
            sig.push(word);
            if (typeof cap === "number" && sig.length >= cap) {
                break;
            }
        }
    }
    return sig;
};

export const concernOf = function concernOf(rule: CatalogRule): string {
    const fromLabel = meaningful(labelOf(rule));
    const fromDescription = fromLabel.length < MIN_SIG ? meaningful(stringField(rule, "description"), DESC_CAP) : [];
    const sig = (fromDescription.length > fromLabel.length ? fromDescription : fromLabel).toSorted((a, b) =>
        a.localeCompare(b),
    );
    if (sig.length === 0) {
        return `${FALLBACK_PREFIX}${tokenizeWords(labelOf(rule)).join("-").slice(0, FALLBACK_SLICE) || rule.ruleId}`;
    }
    return sig.join("-");
};
```
