chore: auto-commit (57 archivos)
- frontend/functions/core/format_datetime_short.md - frontend/functions/core/format_datetime_short.test.ts - frontend/functions/core/format_datetime_short.ts - frontend/functions/core/format_duration.md - frontend/functions/core/format_duration.test.ts - frontend/functions/core/format_duration.ts - frontend/functions/core/month_grid.md - frontend/functions/core/month_grid.test.ts - frontend/functions/core/month_grid.ts - frontend/functions/core/string_hash_palette.md - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: format_datetime_short
|
||||
kind: function
|
||||
lang: ts
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "formatDateTimeShort(iso: string): string"
|
||||
description: "Formatea un string ISO datetime a formato corto \"dd/mm/yy hh:mm\". ISO invalido retorna string vacio."
|
||||
tags: [format, datetime, date, display, utility]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: iso
|
||||
desc: "String ISO 8601 con fecha y hora (ej: \"2026-05-09T14:30:00.000Z\" o \"2026-05-09T14:30:00\"). Cualquier string que `new Date()` no pueda parsear retorna \"\"."
|
||||
output: "String con formato \"dd/mm/yy hh:mm\" usando la hora local del entorno. String vacio si el ISO es invalido."
|
||||
tested: true
|
||||
tests:
|
||||
- "ISO valido retorna dd/mm/yy hh:mm"
|
||||
- "string vacio retorna vacio"
|
||||
- "ISO invalido retorna vacio"
|
||||
- "padding de dia y mes con cero"
|
||||
test_file_path: "frontend/functions/core/format_datetime_short.test.ts"
|
||||
file_path: "frontend/functions/core/format_datetime_short.ts"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```typescript
|
||||
formatDateTimeShort("2026-05-09T14:30:00") // "09/05/26 14:30"
|
||||
formatDateTimeShort("2026-01-01T00:00:00") // "01/01/26 00:00"
|
||||
formatDateTimeShort("not-a-date") // ""
|
||||
formatDateTimeShort("") // ""
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Extraida de `apps/kanban/frontend/src/components/format.ts` lineas 32-41.
|
||||
Usa `Date` nativo del entorno. La hora mostrada es **hora local** del navegador/runtime (getHours, getMinutes).
|
||||
El ano se trunca a 2 digitos (slice(-2)), correcto para fechas 2000-2099.
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { formatDateTimeShort } from "./format_datetime_short";
|
||||
|
||||
// Nota: la funcion usa hora local del entorno (getHours/getMinutes).
|
||||
// Los tests usan fechas con offset conocido o verifican el patron de formato.
|
||||
|
||||
describe("formatDateTimeShort", () => {
|
||||
it("string vacio retorna vacio", () => {
|
||||
expect(formatDateTimeShort("")).toBe("");
|
||||
});
|
||||
|
||||
it("ISO invalido retorna vacio", () => {
|
||||
expect(formatDateTimeShort("not-a-date")).toBe("");
|
||||
expect(formatDateTimeShort("2026-13-01")).toBe("");
|
||||
});
|
||||
|
||||
it("ISO valido retorna dd/mm/yy hh:mm", () => {
|
||||
// Verificamos el patron sin asumir timezone: resultado debe tener forma xx/xx/xx xx:xx
|
||||
const result = formatDateTimeShort("2026-05-09T14:30:00");
|
||||
expect(result).toMatch(/^\d{2}\/\d{2}\/\d{2} \d{2}:\d{2}$/);
|
||||
});
|
||||
|
||||
it("padding de dia y mes con cero", () => {
|
||||
// Dia 1, mes enero: debe ser "01/01/..."
|
||||
const result = formatDateTimeShort("2026-01-01T00:00:00");
|
||||
expect(result).toMatch(/^01\/01\//);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Formatea un string ISO a "dd/mm/yy hh:mm".
|
||||
* Retorna "" si el ISO es invalido o no parseable.
|
||||
*/
|
||||
export function formatDateTimeShort(iso: string): string {
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return "";
|
||||
const dd = String(d.getDate()).padStart(2, "0");
|
||||
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const yy = String(d.getFullYear()).slice(-2);
|
||||
const hh = String(d.getHours()).padStart(2, "0");
|
||||
const mi = String(d.getMinutes()).padStart(2, "0");
|
||||
return `${dd}/${mm}/${yy} ${hh}:${mi}`;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
name: format_duration
|
||||
kind: function
|
||||
lang: ts
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "formatDuration(ms: number): string"
|
||||
description: "Formatea una duracion en milisegundos a string humano escalado. Escala: m | h Xm | D Xh | S XD | M XS. NaN, Infinity o negativo retornan \"0m\"."
|
||||
tags: [format, duration, time, display, utility]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: ms
|
||||
desc: "Duracion en milisegundos. Debe ser un numero finito no negativo. NaN, Infinity o negativos producen \"0m\"."
|
||||
output: "String humano escalado segun magnitud: minutos (Xm), horas+minutos (Xh Ym), dias+horas (XD Yh), semanas+dias (XS YD), meses+semanas (XM YS). Menor de un minuto devuelve \"0m\"."
|
||||
tested: true
|
||||
tests:
|
||||
- "menos de un minuto retorna 0m"
|
||||
- "exactamente 1 hora retorna 1h"
|
||||
- "1h 30m retorna 1h 30m"
|
||||
- "1 dia sin horas retorna 1D"
|
||||
- "2 dias 3 horas retorna 2D 3h"
|
||||
- "1 semana sin dias retorna 1S"
|
||||
- "2 semanas 3 dias retorna 2S 3D"
|
||||
- "1 mes sin semanas retorna 1M"
|
||||
- "2 meses 1 semana retorna 2M 1S"
|
||||
- "NaN retorna 0m"
|
||||
- "negativo retorna 0m"
|
||||
- "Infinity retorna 0m"
|
||||
test_file_path: "frontend/functions/core/format_duration.test.ts"
|
||||
file_path: "frontend/functions/core/format_duration.ts"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```typescript
|
||||
formatDuration(0) // "0m"
|
||||
formatDuration(30_000) // "0m" (30s < 1min)
|
||||
formatDuration(90_000) // "1m"
|
||||
formatDuration(3_600_000) // "1h"
|
||||
formatDuration(5_400_000) // "1h 30m"
|
||||
formatDuration(86_400_000) // "1D"
|
||||
formatDuration(97_200_000) // "1D 3h"
|
||||
formatDuration(604_800_000) // "1S"
|
||||
formatDuration(2_592_000_000) // "1M"
|
||||
formatDuration(NaN) // "0m"
|
||||
formatDuration(-1) // "0m"
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Extraida de `apps/kanban/frontend/src/components/format.ts` lineas 9-30.
|
||||
Funcion pura sin dependencias externas. Usa constantes locales privadas (MIN, HOUR, DAY, WEEK, MONTH).
|
||||
Escala en ingles/abreviada: m=minutos, h=horas, D=dias, S=semanas, M=meses.
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { formatDuration } from "./format_duration";
|
||||
|
||||
const MIN = 60_000;
|
||||
const HOUR = 60 * MIN;
|
||||
const DAY = 24 * HOUR;
|
||||
const WEEK = 7 * DAY;
|
||||
const MONTH = 30 * DAY;
|
||||
|
||||
describe("formatDuration", () => {
|
||||
it("menos de un minuto retorna 0m", () => {
|
||||
expect(formatDuration(0)).toBe("0m");
|
||||
expect(formatDuration(30_000)).toBe("0m");
|
||||
expect(formatDuration(59_999)).toBe("0m");
|
||||
});
|
||||
|
||||
it("exactamente 1 hora retorna 1h", () => {
|
||||
expect(formatDuration(HOUR)).toBe("1h");
|
||||
});
|
||||
|
||||
it("1h 30m retorna 1h 30m", () => {
|
||||
expect(formatDuration(HOUR + 30 * MIN)).toBe("1h 30m");
|
||||
});
|
||||
|
||||
it("1 dia sin horas retorna 1D", () => {
|
||||
expect(formatDuration(DAY)).toBe("1D");
|
||||
});
|
||||
|
||||
it("2 dias 3 horas retorna 2D 3h", () => {
|
||||
expect(formatDuration(2 * DAY + 3 * HOUR)).toBe("2D 3h");
|
||||
});
|
||||
|
||||
it("1 semana sin dias retorna 1S", () => {
|
||||
expect(formatDuration(WEEK)).toBe("1S");
|
||||
});
|
||||
|
||||
it("2 semanas 3 dias retorna 2S 3D", () => {
|
||||
expect(formatDuration(2 * WEEK + 3 * DAY)).toBe("2S 3D");
|
||||
});
|
||||
|
||||
it("1 mes sin semanas retorna 1M", () => {
|
||||
expect(formatDuration(MONTH)).toBe("1M");
|
||||
});
|
||||
|
||||
it("2 meses 1 semana retorna 2M 1S", () => {
|
||||
expect(formatDuration(2 * MONTH + WEEK)).toBe("2M 1S");
|
||||
});
|
||||
|
||||
it("NaN retorna 0m", () => {
|
||||
expect(formatDuration(NaN)).toBe("0m");
|
||||
});
|
||||
|
||||
it("negativo retorna 0m", () => {
|
||||
expect(formatDuration(-1)).toBe("0m");
|
||||
expect(formatDuration(-999)).toBe("0m");
|
||||
});
|
||||
|
||||
it("Infinity retorna 0m", () => {
|
||||
expect(formatDuration(Infinity)).toBe("0m");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
// Escala unidades segun magnitud: m | h Xm | D Xh | S XD | M XS.
|
||||
// <1 minuto cae como "0m" para mantener la unidad mas pequena coherente.
|
||||
const MIN = 60_000;
|
||||
const HOUR = 60 * MIN;
|
||||
const DAY = 24 * HOUR;
|
||||
const WEEK = 7 * DAY;
|
||||
const MONTH = 30 * DAY;
|
||||
|
||||
/**
|
||||
* Formatea una duracion en milisegundos a string humano escalado.
|
||||
*
|
||||
* Escala: m | h Xm | D Xh | S XD | M XS.
|
||||
* NaN, Infinity o negativo retornan "0m".
|
||||
*/
|
||||
export function formatDuration(ms: number): string {
|
||||
if (!Number.isFinite(ms) || ms < 0) return "0m";
|
||||
if (ms < HOUR) return `${Math.floor(ms / MIN)}m`;
|
||||
if (ms < DAY) {
|
||||
const h = Math.floor(ms / HOUR);
|
||||
const m = Math.floor((ms % HOUR) / MIN);
|
||||
return m === 0 ? `${h}h` : `${h}h ${m}m`;
|
||||
}
|
||||
if (ms < WEEK) {
|
||||
const d = Math.floor(ms / DAY);
|
||||
const h = Math.floor((ms % DAY) / HOUR);
|
||||
return h === 0 ? `${d}D` : `${d}D ${h}h`;
|
||||
}
|
||||
if (ms < MONTH) {
|
||||
const w = Math.floor(ms / WEEK);
|
||||
const d = Math.floor((ms % WEEK) / DAY);
|
||||
return d === 0 ? `${w}S` : `${w}S ${d}D`;
|
||||
}
|
||||
const m = Math.floor(ms / MONTH);
|
||||
const w = Math.floor((ms % MONTH) / WEEK);
|
||||
return w === 0 ? `${m}M` : `${m}M ${w}S`;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
name: month_grid
|
||||
kind: function
|
||||
lang: ts
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "monthGrid(year: number, month: number): { date: string | null; inMonth: boolean }[]"
|
||||
description: "Genera la cuadricula mensual con inicio en lunes (Mon-first ISO). Retorna array de celdas multiplo de 7 con padding nulo al inicio y final. Util para calendarios y vistas de mes."
|
||||
tags: [calendar, date, grid, month, utility]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: year
|
||||
desc: "Ano completo del mes a generar (ej: 2026). Rango valido: cualquier ano soportado por Date nativo."
|
||||
- name: month
|
||||
desc: "Mes 1-12 donde 1=Enero y 12=Diciembre."
|
||||
output: "Array de MonthGridCell ({ date: string|null; inMonth: boolean }) con longitud siempre multiplo de 7. Celdas de padding tienen date=null e inMonth=false. Celdas del mes tienen date en formato YYYY-MM-DD e inMonth=true."
|
||||
tested: true
|
||||
tests:
|
||||
- "enero 2026 empieza en jueves (dow=3)"
|
||||
- "febrero 2024 bisiesto tiene 29 dias"
|
||||
- "longitud siempre multiplo de 7"
|
||||
- "primer dia inMonth es el primero del mes"
|
||||
- "ultimo dia inMonth es el ultimo del mes"
|
||||
- "celdas de padding tienen date null"
|
||||
- "mes que empieza en lunes no tiene padding inicial"
|
||||
test_file_path: "frontend/functions/core/month_grid.test.ts"
|
||||
file_path: "frontend/functions/core/month_grid.ts"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```typescript
|
||||
const cells = monthGrid(2026, 5); // Mayo 2026 (1 mayo = viernes => firstDow=4)
|
||||
// cells.length es multiplo de 7 (ej: 35)
|
||||
// cells[0..3] => { date: null, inMonth: false } (padding lun-jue)
|
||||
// cells[4] => { date: "2026-05-01", inMonth: true }
|
||||
// cells[34] => { date: "2026-05-31", inMonth: true }
|
||||
|
||||
monthGrid(2026, 1); // Enero 2026: jueves=dow 3, 3 celdas padding inicial
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Extraida de `apps/kanban/frontend/src/components/CalendarView.tsx` lineas 72-84.
|
||||
Reemplaza la dependencia de `dayjs` por `Date` nativo para mantener la funcion sin dependencias externas.
|
||||
Algoritmo: `firstDow = (new Date(year, month-1, 1).getDay() + 6) % 7` convierte domingo=0 de JS a lunes=0 ISO.
|
||||
`new Date(year, month, 0).getDate()` obtiene el ultimo dia del mes (dia 0 del mes siguiente = ultimo del actual).
|
||||
Exporta tambien el tipo `MonthGridCell` para uso con TypeScript estricto.
|
||||
@@ -0,0 +1,55 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { monthGrid } from "./month_grid";
|
||||
|
||||
describe("monthGrid", () => {
|
||||
it("longitud siempre multiplo de 7", () => {
|
||||
for (let m = 1; m <= 12; m++) {
|
||||
expect(monthGrid(2026, m).length % 7).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("enero 2026 empieza en jueves (dow=3)", () => {
|
||||
// 1 enero 2026 es jueves. Mon-first => dow=3 (0=lun,1=mar,2=mie,3=jue)
|
||||
const cells = monthGrid(2026, 1);
|
||||
expect(cells[0].date).toBeNull();
|
||||
expect(cells[1].date).toBeNull();
|
||||
expect(cells[2].date).toBeNull();
|
||||
expect(cells[3].date).toBe("2026-01-01");
|
||||
expect(cells[3].inMonth).toBe(true);
|
||||
});
|
||||
|
||||
it("febrero 2024 bisiesto tiene 29 dias", () => {
|
||||
const cells = monthGrid(2024, 2);
|
||||
const inMonth = cells.filter((c) => c.inMonth);
|
||||
expect(inMonth).toHaveLength(29);
|
||||
expect(inMonth[28].date).toBe("2024-02-29");
|
||||
});
|
||||
|
||||
it("primer dia inMonth es el primero del mes", () => {
|
||||
const cells = monthGrid(2026, 5);
|
||||
const first = cells.find((c) => c.inMonth);
|
||||
expect(first?.date).toBe("2026-05-01");
|
||||
});
|
||||
|
||||
it("ultimo dia inMonth es el ultimo del mes", () => {
|
||||
const cells = monthGrid(2026, 5); // mayo tiene 31 dias
|
||||
const last = [...cells].reverse().find((c) => c.inMonth);
|
||||
expect(last?.date).toBe("2026-05-31");
|
||||
});
|
||||
|
||||
it("celdas de padding tienen date null", () => {
|
||||
const cells = monthGrid(2026, 1);
|
||||
const padding = cells.filter((c) => !c.inMonth);
|
||||
for (const p of padding) {
|
||||
expect(p.date).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("mes que empieza en lunes no tiene padding inicial", () => {
|
||||
// Buscamos un mes que empiece en lunes. Enero 2024: lunes.
|
||||
// 1 enero 2024 es lunes => firstDow=0 => no padding inicial
|
||||
const cells = monthGrid(2024, 1);
|
||||
expect(cells[0].date).toBe("2024-01-01");
|
||||
expect(cells[0].inMonth).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
export interface MonthGridCell {
|
||||
date: string | null;
|
||||
inMonth: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera la cuadricula mensual con inicio en lunes (ISO week).
|
||||
*
|
||||
* @param year Año completo (ej: 2026).
|
||||
* @param month Mes 1-12 (Enero = 1).
|
||||
* @returns Array de celdas con longitud multiplo de 7.
|
||||
* Celdas de padding tienen date=null e inMonth=false.
|
||||
* Celdas del mes tienen date="YYYY-MM-DD" e inMonth=true.
|
||||
*/
|
||||
export function monthGrid(year: number, month: number): MonthGridCell[] {
|
||||
// getDay() devuelve 0=Dom..6=Sab. Convertir a 0=Lun..6=Dom.
|
||||
const firstDow = (new Date(year, month - 1, 1).getDay() + 6) % 7;
|
||||
const lastDay = new Date(year, month, 0).getDate();
|
||||
|
||||
const cells: MonthGridCell[] = [];
|
||||
|
||||
// Padding inicial
|
||||
for (let i = 0; i < firstDow; i++) {
|
||||
cells.push({ date: null, inMonth: false });
|
||||
}
|
||||
|
||||
// Dias del mes
|
||||
for (let d = 1; d <= lastDay; d++) {
|
||||
const mm = String(month).padStart(2, "0");
|
||||
const dd = String(d).padStart(2, "0");
|
||||
cells.push({ date: `${year}-${mm}-${dd}`, inMonth: true });
|
||||
}
|
||||
|
||||
// Padding final hasta multiplo de 7
|
||||
while (cells.length % 7 !== 0) {
|
||||
cells.push({ date: null, inMonth: false });
|
||||
}
|
||||
|
||||
return cells;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: string_hash_palette
|
||||
kind: function
|
||||
lang: ts
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "stringHashPalette(s: string, palette: string[]): string"
|
||||
description: "Mapea un string a un elemento de la paleta usando hash DJB-31 deterministico (h = (h*31 + charCodeAt) >>> 0). Util para asignar colores consistentes a tags, usuarios u otros identificadores de texto."
|
||||
tags: [hash, color, palette, string, deterministic, utility]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: s
|
||||
desc: "String de entrada a hashear. Cualquier string Unicode. String vacio produce h=0, retorna palette[0]."
|
||||
- name: palette
|
||||
desc: "Array de strings (colores u otros valores) a usar como paleta. No debe estar vacio (lanza Error si lo esta)."
|
||||
output: "Un elemento de palette seleccionado deterministicamente por h % palette.length. El mismo string siempre retorna el mismo elemento para la misma paleta."
|
||||
tested: true
|
||||
tests:
|
||||
- "string vacio retorna palette[0]"
|
||||
- "mismo string siempre retorna mismo color"
|
||||
- "strings distintos pueden dar colores distintos"
|
||||
- "paleta vacia lanza error"
|
||||
- "paleta de un elemento siempre retorna ese elemento"
|
||||
test_file_path: "frontend/functions/core/string_hash_palette.test.ts"
|
||||
file_path: "frontend/functions/core/string_hash_palette.ts"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```typescript
|
||||
const TAG_PALETTE = ["blue", "cyan", "teal", "green", "lime", "yellow", "orange", "red", "pink", "grape", "violet", "indigo"];
|
||||
|
||||
stringHashPalette("frontend", TAG_PALETTE) // deterministico, ej: "teal"
|
||||
stringHashPalette("backend", TAG_PALETTE) // deterministico, diferente
|
||||
stringHashPalette("frontend", TAG_PALETTE) // mismo resultado siempre
|
||||
|
||||
stringHashPalette("x", []) // throws Error("palette must not be empty")
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Extraida de `apps/kanban/frontend/src/components/colors.ts` lineas 70-76 (funcion `tagColor`).
|
||||
Generalizada para aceptar cualquier paleta en vez de la constante `TAG_PALETTE` hardcodeada.
|
||||
El hash DJB-31 (variante de DJB2 con multiplicador 31) produce distribucion uniforme suficiente para asignacion de colores. No es criptografico.
|
||||
El operador `>>> 0` garantiza entero de 32 bits sin signo, evitando overflow en strings largos.
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { stringHashPalette } from "./string_hash_palette";
|
||||
|
||||
const PALETTE = ["blue", "cyan", "teal", "green", "lime", "yellow", "orange", "red", "pink", "grape", "violet", "indigo"];
|
||||
|
||||
describe("stringHashPalette", () => {
|
||||
it("string vacio retorna palette[0]", () => {
|
||||
// h empieza en 0, loop no itera, h%n = 0 => palette[0]
|
||||
expect(stringHashPalette("", PALETTE)).toBe(PALETTE[0]);
|
||||
});
|
||||
|
||||
it("mismo string siempre retorna mismo color", () => {
|
||||
const a = stringHashPalette("frontend", PALETTE);
|
||||
const b = stringHashPalette("frontend", PALETTE);
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
it("strings distintos pueden dar colores distintos", () => {
|
||||
const colors = new Set(["alpha", "beta", "gamma", "delta", "epsilon"].map((s) => stringHashPalette(s, PALETTE)));
|
||||
// Con 5 strings distintos y paleta de 12 es muy probable obtener al menos 2 distintos
|
||||
expect(colors.size).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it("paleta vacia lanza error", () => {
|
||||
expect(() => stringHashPalette("hello", [])).toThrow("palette must not be empty");
|
||||
});
|
||||
|
||||
it("paleta de un elemento siempre retorna ese elemento", () => {
|
||||
expect(stringHashPalette("anything", ["only"])).toBe("only");
|
||||
expect(stringHashPalette("", ["only"])).toBe("only");
|
||||
expect(stringHashPalette("xyz", ["only"])).toBe("only");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Mapea un string a un color de la paleta usando hash DJB-31 deterministico.
|
||||
*
|
||||
* Hash: h = (h * 31 + charCodeAt(i)) >>> 0 (unsigned 32-bit).
|
||||
* El indice es h % palette.length.
|
||||
*
|
||||
* @throws Error si palette esta vacio.
|
||||
*/
|
||||
export function stringHashPalette(s: string, palette: string[]): string {
|
||||
if (palette.length === 0) throw new Error("palette must not be empty");
|
||||
let h = 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
h = (h * 31 + s.charCodeAt(i)) >>> 0;
|
||||
}
|
||||
return palette[h % palette.length];
|
||||
}
|
||||
Reference in New Issue
Block a user