From 37e7773dd36c4d4a80031fca441f0b6dc6d4b509 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 2 Oct 2024 14:26:56 -0400 Subject: [PATCH] Add utils for ranking and finding labels (#2) --- CHANGELOG.md | 17 ++++ .../src/arches_vue_utils/constants.ts | 3 + .../src/arches_vue_utils/types.ts | 16 ++++ .../src/arches_vue_utils/utils.spec.ts | 92 +++++++++++++++++++ .../src/arches_vue_utils/utils.ts | 64 +++++++++++++ 5 files changed, 192 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 arches_vue_utils/src/arches_vue_utils/constants.ts create mode 100644 arches_vue_utils/src/arches_vue_utils/utils.spec.ts create mode 100644 arches_vue_utils/src/arches_vue_utils/utils.ts diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cef85b7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Add utils for ranking and finding labels + +### Deprecated + +### Removed + +### Security diff --git a/arches_vue_utils/src/arches_vue_utils/constants.ts b/arches_vue_utils/src/arches_vue_utils/constants.ts new file mode 100644 index 0000000..d65fc82 --- /dev/null +++ b/arches_vue_utils/src/arches_vue_utils/constants.ts @@ -0,0 +1,3 @@ +export const PREF_LABEL = "prefLabel"; +export const ALT_LABEL = "altLabel"; +export const HIDDEN_LABEL = "hiddenLabel"; diff --git a/arches_vue_utils/src/arches_vue_utils/types.ts b/arches_vue_utils/src/arches_vue_utils/types.ts index 321e9d6..77fbde1 100644 --- a/arches_vue_utils/src/arches_vue_utils/types.ts +++ b/arches_vue_utils/src/arches_vue_utils/types.ts @@ -6,3 +6,19 @@ export interface Language { name: string; scope: string; } + +export interface Label { + value: string; + language_id: string; + valuetype_id: string; +} + +export interface WithLabels { + labels: Label[]; +} + +export interface WithValues { + values: Label[]; +} + +export type Labellable = WithLabels | WithValues; diff --git a/arches_vue_utils/src/arches_vue_utils/utils.spec.ts b/arches_vue_utils/src/arches_vue_utils/utils.spec.ts new file mode 100644 index 0000000..c83ae3b --- /dev/null +++ b/arches_vue_utils/src/arches_vue_utils/utils.spec.ts @@ -0,0 +1,92 @@ +import { + ALT_LABEL, + HIDDEN_LABEL, + PREF_LABEL, +} from "@/arches_vue_utils/constants.ts"; +import { getItemLabel, rankLabel } from "@/arches_vue_utils/utils.ts"; + +import type { Label } from "@/arches_vue_utils/types"; + +// Test utils +function asLabel(valuetype_id: string, language_id: string): Label { + return { + value: "arbitrary", + valuetype_id, + language_id, + }; +} + +const systemLanguageCode = "en-ZA"; // arbitrary + +describe("rankLabel() util", () => { + const rank = ( + valuetype_id: string, + labelLanguageCode: string, + desiredLanguageCode: string, + ) => + rankLabel( + asLabel(valuetype_id, labelLanguageCode), + desiredLanguageCode, + systemLanguageCode, + ); + + // Test cases inspired from python module + it("Prefers explicit region", () => { + expect(rank(PREF_LABEL, "fr-CA", "fr-CA")).toBeGreaterThan( + rank(PREF_LABEL, "fr", "fr-CA"), + ); + }); + it("Prefers pref over alt", () => { + expect(rank(PREF_LABEL, "fr", "fr-CA")).toBeGreaterThan( + rank(ALT_LABEL, "fr", "fr-CA"), + ); + }); + it("Prefers alt over hidden", () => { + expect(rank(ALT_LABEL, "fr", "fr-CA")).toBeGreaterThan( + rank(HIDDEN_LABEL, "fr", "fr-CA"), + ); + }); + it("Prefers alt label in system language to anything else", () => { + expect(rank(ALT_LABEL, systemLanguageCode, "en")).toBeGreaterThan( + rank(PREF_LABEL, "de", "en"), + ); + }); + it("Prefers region-insensitive match in system language", () => { + expect(rank(PREF_LABEL, "en", "de")).toBeGreaterThan( + rank(PREF_LABEL, "fr", "de"), + ); + }); +}); + +describe("getItemLabel() util", () => { + it("Errors if no labels", () => { + expect(() => + getItemLabel( + { labels: [] }, + systemLanguageCode, + systemLanguageCode, + ), + ).toThrow(); + expect(() => + getItemLabel( + { values: [] }, + systemLanguageCode, + systemLanguageCode, + ), + ).toThrow(); + }); + it("Falls back to system language", () => { + expect( + getItemLabel( + { + labels: [ + asLabel(PREF_LABEL, "de"), + asLabel(PREF_LABEL, systemLanguageCode), + ], + }, + "fr", + systemLanguageCode, + ).language_id, + ).toEqual(systemLanguageCode); + }); +}); diff --git a/arches_vue_utils/src/arches_vue_utils/utils.ts b/arches_vue_utils/src/arches_vue_utils/utils.ts new file mode 100644 index 0000000..3ffabde --- /dev/null +++ b/arches_vue_utils/src/arches_vue_utils/utils.ts @@ -0,0 +1,64 @@ +import { ALT_LABEL, PREF_LABEL } from "@/arches_vue_utils/constants.ts"; + +import type { + Label, + Labellable, + WithLabels, + WithValues, +} from "@/arches_vue_utils/types"; + +/* Port of rank_label in arches.app.utils.i18n python module */ +export const rankLabel = ( + label: Label, + preferredLanguageCode: string, + systemLanguageCode: string, +): number => { + let rank = 1; + if (label.valuetype_id === PREF_LABEL) { + rank = 10; + } else if (label.valuetype_id === ALT_LABEL) { + rank = 4; + } + + // Some arches deployments may not have standardized capitalizations. + const labelLanguageFull = label.language_id.toLowerCase(); + const labelLanguageNoRegion = label.language_id + .split(/[-_]/)[0] + .toLowerCase(); + const preferredLanguageFull = preferredLanguageCode.toLowerCase(); + const preferredLanguageNoRegion = preferredLanguageCode + .split(/[-_]/)[0] + .toLowerCase(); + const systemLanguageFull = systemLanguageCode.toLowerCase(); + const systemLanguageNoRegion = systemLanguageCode + .split(/[-_]/)[0] + .toLowerCase(); + + if (labelLanguageFull === preferredLanguageFull) { + rank *= 10; + } else if (labelLanguageNoRegion === preferredLanguageNoRegion) { + rank *= 5; + } else if (labelLanguageFull === systemLanguageFull) { + rank *= 3; + } else if (labelLanguageNoRegion === systemLanguageNoRegion) { + rank *= 2; + } + + return rank; +}; + +export const getItemLabel = ( + item: Labellable, + preferredLanguageCode: string, + systemLanguageCode: string, +): Label => { + const labels = (item as WithLabels).labels ?? (item as WithValues).values; + if (!labels.length) { + throw new Error(); + } + return labels.sort( + (a, b) => + rankLabel(b, preferredLanguageCode, systemLanguageCode) - + rankLabel(a, preferredLanguageCode, systemLanguageCode), + )[0]; +};