diff --git a/static/js/tasks.js b/static/js/tasks.js index a4ffc63..73aa39c 100644 --- a/static/js/tasks.js +++ b/static/js/tasks.js @@ -7,6 +7,7 @@ import markdownModule from './markdown.js'; import * as spinnerModule from './spinner.js'; import { makeWindowDraggable } from './windowDrag.js'; import { sortModelIds } from './modelSort.js'; +import { ordinalSuffix } from './util/ordinal.js'; const API_BASE = window.location.origin; let _open = false; @@ -244,7 +245,7 @@ function _scheduleLabel(task) { } if (task.schedule === 'monthly') { const d = task.scheduled_day ?? 1; - const suffix = d === 1 ? 'st' : d === 2 ? 'nd' : d === 3 ? 'rd' : 'th'; + const suffix = ordinalSuffix(d); return `Monthly on ${d}${suffix} at ${localTime}`; } return task.schedule || '—'; diff --git a/static/js/util/ordinal.js b/static/js/util/ordinal.js new file mode 100644 index 0000000..20c37d4 --- /dev/null +++ b/static/js/util/ordinal.js @@ -0,0 +1,13 @@ +// Pure (browser-free) English ordinal suffix, e.g. 1 -> "st", 21 -> "st", +// 22 -> "nd", 23 -> "rd", 11/12/13 -> "th". Extracted so it can be unit-tested. +export function ordinalSuffix(n) { + const a = Math.abs(Math.trunc(Number(n) || 0)); + const mod100 = a % 100; + if (mod100 >= 11 && mod100 <= 13) return 'th'; + switch (a % 10) { + case 1: return 'st'; + case 2: return 'nd'; + case 3: return 'rd'; + default: return 'th'; + } +} diff --git a/tests/test_ordinal_suffix_js.py b/tests/test_ordinal_suffix_js.py new file mode 100644 index 0000000..54f90f4 --- /dev/null +++ b/tests/test_ordinal_suffix_js.py @@ -0,0 +1,35 @@ +"""Pin the ordinal-suffix helper used by the monthly-schedule label in tasks.js. + +_scheduleLabel built the suffix with `d === 1 ? 'st' : d === 2 ? 'nd' : ...`, +which only handles single digits, so a monthly task on day 21/22/23/31 rendered +"Monthly on 21th"/"22th"/"23th"/"31th". The shared ordinalSuffix() fixes this. +""" +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +_REPO = Path(__file__).resolve().parent.parent +_HELPER = _REPO / "static" / "js" / "util" / "ordinal.js" +_HAS_NODE = shutil.which("node") is not None + + +def _suffixes(nums): + arr = json.dumps(nums) + js = f""" + import {{ ordinalSuffix }} from '{_HELPER.as_posix()}'; + console.log(JSON.stringify({arr}.map(n => n + ordinalSuffix(n)))); + """ + proc = subprocess.run(["node", "--input-type=module"], input=js, + capture_output=True, text=True, cwd=str(_REPO), timeout=30) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout.strip()) + + +@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH") +def test_ordinal_suffixes_for_days_of_month(): + assert _suffixes([1, 2, 3, 4, 11, 12, 13, 21, 22, 23, 31]) == [ + "1st", "2nd", "3rd", "4th", "11th", "12th", "13th", "21st", "22nd", "23rd", "31st", + ]