Implement octave-vs-octave precedence, and fix a real day-label bug it surfaced
Deploy / deploy (push) Successful in 52s
Deploy / deploy (push) Successful in 52s
When more than one octave is active on a day with no temporal standing of
its own (St. Lawrence's and the Assumption's genuinely overlap every Aug
16-17), which one governs the day's content/label is now decided by rank
comparison instead of "whichever started first" (an accident of
insertion order). Per direct instruction:
- Highest effective rank wins outright; every other active octave still
gets commemorated, same as a single octave already outranking a weak
rival saint.
- A rank tie goes to whichever octave started more recently -- day 1 of
a new octave needs to be fully present, the whole point of it
starting. No real tied-rank case exists yet to verify this
empirically, unlike everything else here -- documented in TODO.md as
a stated decision, not a live finding.
New OctaveConfig.closingDayRank (default duplex): an octave's own final
day ("in Octava") is elevated above its ordinary in-between rank --
live-verified as a real, general pattern (both St. Lawrence's Aug 17 and
the Assumption's Aug 22 show as Duplex, above their otherwise-Semiduplex
ordinary days), not a one-off. This elevation is *why* Lawrence's octave
beats the Assumption's on their one real overlap day despite the
Assumption being the far higher-ranked feast overall. Feeds both the
existing rival-saint threshold and the new octave-vs-octave comparison.
calendar/octaves.ts gains resolveActiveOctave (+ pickWinningOctave, the
comparison itself factored out for direct unit testing against synthetic
data, since no real tied-rank overlap exists to test against yet).
hours/resolve-common.ts's resolveOfficeWinner and calendar/day-label.ts
both now call it instead of each keeping their own "activeOctavesFor(...)
[0]" logic.
That consolidation surfaced a real, independent bug: getDayLabel never
checked temporalCategory at all before choosing an octave name, unlike
resolveOfficeWinner -- found while testing the real Aug 16 overlap (a
Sunday that year, where the temporal Sunday has standing and should win
outright). Live-verified counterexample: the Christmas Octave's own
stack (Dec 30) was wrongly labeled "3rd Day within the Octave of The Holy
Innocents" instead of the correct plain temporal label -- the real title
never names any of the four stacked octaves there. Fixed by sharing the
exact same ordinary-feria gate resolveOfficeWinner already had, so the
two can no longer disagree.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -67,40 +67,54 @@ for Terce/Sext/None, antiphon only for Prime (its chapter needed no new
|
||||
file — see above). The same 31-saint backlog above applies here too, one
|
||||
id at a time, as each gets a Lauds override.
|
||||
|
||||
### Overlapping octaves — real gap found, not yet fixed
|
||||
### Overlapping octaves — done (2026-08)
|
||||
|
||||
`resolveOfficeWinner` (and `getDayLabel`) currently pick whichever active
|
||||
octave started *first* (`activeOctavesFor`'s own ordering) whenever more
|
||||
than one is active on a day with no temporal standing of its own — purely
|
||||
an accident of insertion order, not a real precedence rule. St. Lawrence's
|
||||
octave (Aug 10-17) and the Assumption's octave (Aug 15-22) genuinely
|
||||
overlap on Aug 16-17 every year, so this isn't a hypothetical.
|
||||
Was: `resolveOfficeWinner`/`getDayLabel` picked whichever active octave
|
||||
started *first* whenever more than one was active — an accident of
|
||||
insertion order, not a real precedence rule. St. Lawrence's octave (Aug
|
||||
10-17) and the Assumption's (Aug 15-22) genuinely overlap on Aug 16-17
|
||||
every year, so this wasn't hypothetical.
|
||||
|
||||
Live-checked both overlap days (Monastic Tridentinum 1617, 2026): Aug 16
|
||||
that year happens to be a Sunday, so the *temporal* Sunday wins outright
|
||||
and both octaves are merely commemorated (already correctly handled by
|
||||
the existing `ordinary-feria` gate — not actually a two-octave contest at
|
||||
all). Aug 17 is the real case: "**In Octava S. Laurentii Martyris ~
|
||||
Duplex**" wins outright, with "*Tertia die infra Octavam S. Assumptionis*"
|
||||
merely commemorated — Lawrence's octave beats the Assumption's despite the
|
||||
Assumption being the far higher-ranked feast overall (Duplex I. classis vs.
|
||||
Lawrence's Duplex II. classis). Why: Aug 17 is specifically Lawrence's own
|
||||
**final octave day** ("in Octava," the 8th and closing day), which real
|
||||
practice elevates to its own higher rank (Duplex here, above the ordinary
|
||||
Semiduplex of the days in between) — a concept this app doesn't model at
|
||||
all yet (`OctaveConfig` has no notion of an elevated final day, every day
|
||||
of an octave is currently treated as uniform strength).
|
||||
Design (per direct instruction, not independently re-derived from a
|
||||
primary source the way most of this project is): when more than one
|
||||
octave is active on a day with no temporal standing of its own to yield
|
||||
to first, compare them by rank — highest wins outright, exactly like a
|
||||
single octave already out-ranks a weak rival saint; every other active
|
||||
octave still gets commemorated, never silently dropped. A rank tie goes
|
||||
to whichever octave started more *recently* — reasoning given: day 1 of
|
||||
a newly started octave needs to be fully present, which is the whole
|
||||
point of an octave starting, so the older one yields instead. No real
|
||||
tied-rank case exists yet in this app's calendar to verify that
|
||||
empirically, unlike almost everything else in this project — flagged as
|
||||
a stated, unverified-by-evidence design decision, not a live finding.
|
||||
|
||||
Proposed design (not yet built, needs a decision): when multiple octaves
|
||||
are active with no temporal standing to yield to, compare them by rank
|
||||
the same way a single octave already compares against a rival saint —
|
||||
highest rank wins outright, the rest are commemorated instead of silently
|
||||
dropped to "whichever came first." Needs `OctaveConfig` to carry an
|
||||
elevated final-day rank (distinct from the ordinary in-between-days rank)
|
||||
to get Aug 17 right for a real reason rather than by lucky insertion
|
||||
order. Low urgency in practice (currently only this one real overlap
|
||||
pair), but worth fixing before adding more octaves that could collide the
|
||||
same way.
|
||||
Also needed `OctaveConfig.closingDayRank` (default `duplex`): an
|
||||
octave's own *closing* day ("in Octava," the 8th/final day) is elevated
|
||||
above its ordinary in-between-days rank — live-verified as a real,
|
||||
general pattern (not a St. Lawrence quirk): both his own Aug 17 and the
|
||||
Assumption's own Aug 22 show as Duplex, above their otherwise-Semiduplex
|
||||
ordinary days. This is *why* Lawrence's octave beats the Assumption's on
|
||||
their one real overlap day (Aug 17) despite the Assumption being the far
|
||||
higher-ranked feast overall (Duplex I. classis vs. Lawrence's Duplex II.
|
||||
classis) — his closing-day elevation (Duplex) beats her ordinary day-3
|
||||
strength (Semiduplex). Feeds both `strictestThreshold` (a rival saint
|
||||
now needs Duplex+, not just Semiduplex+, to beat an octave specifically
|
||||
on its own closing day) and the new octave-vs-octave comparison
|
||||
(`calendar/octaves.ts`'s `resolveActiveOctave`/`pickWinningOctave`) —
|
||||
same underlying "how strong is this octave today" question, two
|
||||
different comparison partners.
|
||||
|
||||
`getDayLabel` was carrying its own separate, never-actually-correct
|
||||
octave-selection logic that additionally never checked
|
||||
`temporalCategory` at all — found while testing the real Aug 16 overlap
|
||||
(a Sunday that year): it was showing an octave name even though the
|
||||
*temporal* Sunday has real standing and wins outright, live-verified
|
||||
counterexample being the Christmas Octave's own stack (Dec 30), which
|
||||
was wrongly showing "3rd Day within the Octave of The Holy Innocents"
|
||||
instead of the correct plain temporal label. Fixed by having
|
||||
`getDayLabel` share the exact same `ordinary-feria` gate and
|
||||
`resolveActiveOctave` call as `resolveOfficeWinner`, so the two can never
|
||||
disagree about which identity governs a day.
|
||||
|
||||
### Benedictus antiphons for temporal (non-sanctoral) days — done
|
||||
|
||||
|
||||
+21
-11
@@ -14,7 +14,7 @@ import { easterSunday } from './easter';
|
||||
import { adventStart, firstSundayStrictlyAfter, sundayOnOrBefore } from './temporal';
|
||||
import { addDays, daysBetween, toIsoDate } from './date-math';
|
||||
import { getTemporalFeastRecord } from './temporal-feasts';
|
||||
import { activeOctavesFor, type ActiveOctave } from './octaves';
|
||||
import { resolveActiveOctave, type ActiveOctave } from './octaves';
|
||||
|
||||
function capitalize(text: string): string {
|
||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||
@@ -186,16 +186,26 @@ export function getDayLabel(day: LiturgicalDay): string {
|
||||
return commemoratedSaint ? `${commemoratedSaint.name} — ${anchorName}` : anchorName;
|
||||
}
|
||||
|
||||
// An active octave (St. Lawrence's, Christmas's own stack, ...) is this
|
||||
// day's real primary identity in the live engine, not a footnote — e.g.
|
||||
// "Tertia die infra Octavam S. Laurentii Martyris", not "Wednesday in
|
||||
// the 11th week after Trinity". Picks the oldest-started octave when
|
||||
// several are stacked (activeOctavesFor's own ordering) as the headline;
|
||||
// the others still ride along as ordinary octave commemorations, just
|
||||
// not separately named here.
|
||||
const octaves = activeOctavesFor(day.date);
|
||||
if (octaves.length > 0) {
|
||||
const primary = octaveLabel(octaves[0]!);
|
||||
// An active octave (St. Lawrence's, ...) is this day's real primary
|
||||
// identity in the live engine, not a footnote — e.g. "Tertia die infra
|
||||
// Octavam S. Laurentii Martyris", not "Wednesday in the 11th week after
|
||||
// Trinity" — but *only* when the temporal day itself has no standing of
|
||||
// its own (`ordinary-feria`), same gate as hours/resolve-common.ts's
|
||||
// resolveOfficeWinner and for the same reason: live-verified
|
||||
// counterexample is the Christmas Octave's own stack (Dec 30, e.g.,
|
||||
// `privileged-feria-minor`), where the real title stays the temporal
|
||||
// Sunday's own ("De Dominica Infra Octavam Nativitatis") with no octave
|
||||
// name in it at all — this label agreeing with resolveOfficeWinner
|
||||
// about which one wins is what makes "the office is Lawrence's" and
|
||||
// "the label says Lawrence" consistent instead of two independent
|
||||
// guesses that can disagree. When more than one octave is active at
|
||||
// once (resolveActiveOctave), the highest-ranked wins the headline
|
||||
// (ties broken by whichever started more recently) — the others still
|
||||
// ride along as ordinary octave commemorations, just not separately
|
||||
// named here.
|
||||
const activeOctave = day.temporalCategory === 'ordinary-feria' ? resolveActiveOctave(day.date) : undefined;
|
||||
if (activeOctave) {
|
||||
const primary = octaveLabel(activeOctave);
|
||||
return commemoratedSaint ? `${primary} — ${commemoratedSaint.name}` : primary;
|
||||
}
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@ export function isSundayOrFeast(day: LiturgicalDay): boolean {
|
||||
export { compareFeastClass, isAtLeast, decideOccurrence } from './commemorations';
|
||||
export { resolveCollision } from './collision';
|
||||
export { resolveTemporalId } from './temporal-id';
|
||||
export { activeOctavesFor, strictestThreshold } from './octaves';
|
||||
export { activeOctavesFor, strictestThreshold, resolveActiveOctave } from './octaves';
|
||||
export type { ActiveOctave } from './octaves';
|
||||
export type {
|
||||
LiturgicalDay,
|
||||
|
||||
+58
-2
@@ -17,7 +17,13 @@ import { compareFeastClass } from './commemorations';
|
||||
export interface ActiveOctave {
|
||||
id: string;
|
||||
name: string;
|
||||
/** Rank threshold below which the occurring saint loses the day to this octave. */
|
||||
/** This octave's own effective rank *today* — the ordinary `wins`
|
||||
* threshold on every day except its own closing day, where it's
|
||||
* `closingDayRank` instead (see calendar/types.ts's OctaveConfig).
|
||||
* Doubles as both "how strong a rival saint must be to displace this
|
||||
* octave" and "this octave's own strength when compared against
|
||||
* another simultaneously-active octave" (resolveActiveOctave) — same
|
||||
* underlying question, two different comparison partners. */
|
||||
wins: FeastClass;
|
||||
/** 1 on the feast's own day, counting up from there. */
|
||||
dayNumber: number;
|
||||
@@ -25,6 +31,7 @@ export interface ActiveOctave {
|
||||
|
||||
const DEFAULT_DAYS = 8;
|
||||
const DEFAULT_WINS: FeastClass = 'semiduplex';
|
||||
const DEFAULT_CLOSING_DAY_RANK: FeastClass = 'duplex';
|
||||
/** How far back to look for an octave's own start date — must cover the
|
||||
* longest configured `days` a caller might use; 7 covers the standard
|
||||
* 8-day octave (day 1 = the start itself, day 8 = 7 days later). */
|
||||
@@ -48,7 +55,10 @@ function considerCandidate(
|
||||
return;
|
||||
}
|
||||
seen.add(id);
|
||||
active.push({ id, name, wins: octave.wins ?? DEFAULT_WINS, dayNumber: offset + 1 });
|
||||
const dayNumber = offset + 1;
|
||||
const isClosingDay = dayNumber === days;
|
||||
const wins = isClosingDay ? (octave.closingDayRank ?? DEFAULT_CLOSING_DAY_RANK) : (octave.wins ?? DEFAULT_WINS);
|
||||
active.push({ id, name, wins, dayNumber });
|
||||
}
|
||||
|
||||
/** Every octave (sanctoral or temporal) whose window covers `isoDate`,
|
||||
@@ -89,3 +99,49 @@ export function strictestThreshold(octaves: ActiveOctave[]): FeastClass {
|
||||
octaves[0]?.wins ?? DEFAULT_WINS,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Which single active octave actually governs a day's own content/label
|
||||
* when more than one is active at once (this app's first real case: St.
|
||||
* Lawrence's, Aug 10-17, and the Assumption's, Aug 15-22, genuinely
|
||||
* overlap every year) and no rival saint has already displaced all of
|
||||
* them outright (that's calendar/index.ts's applyOctaves — this only
|
||||
* runs octave-vs-octave). Per direct instruction:
|
||||
*
|
||||
* - Highest effective rank (`wins`, already elevated on either octave's
|
||||
* own closing day) wins outright.
|
||||
* - Tied rank: the more recently *started* octave wins (smaller
|
||||
* `dayNumber` today) — the reasoning given was that day 1 of a newly
|
||||
* started octave needs to be fully present, which is the whole point
|
||||
* of it starting; the older octave that's already been running is
|
||||
* commemorated instead, same as any octave that loses this comparison.
|
||||
*
|
||||
* Undefined when no octave is active at all. Every other active octave
|
||||
* still gets commemorated regardless of which one wins here — this
|
||||
* function only decides whose *content* (and day-label name) governs,
|
||||
* not who gets left out of the commemoration list entirely (see
|
||||
* calendar/index.ts's applyOctaves, unchanged by this).
|
||||
*/
|
||||
export function resolveActiveOctave(isoDate: string): ActiveOctave | undefined {
|
||||
return pickWinningOctave(activeOctavesFor(isoDate));
|
||||
}
|
||||
|
||||
/** The comparison itself, factored out from resolveActiveOctave so the
|
||||
* precedence rule (rank, then recency) is directly unit-testable against
|
||||
* synthetic ActiveOctave data — no real equal-rank overlap exists yet in
|
||||
* this app's own calendar to exercise the tie-break against. */
|
||||
export function pickWinningOctave(active: ActiveOctave[]): ActiveOctave | undefined {
|
||||
return active.reduce<ActiveOctave | undefined>((best, candidate) => {
|
||||
if (!best) {
|
||||
return candidate;
|
||||
}
|
||||
const rankCmp = compareFeastClass(candidate.wins, best.wins);
|
||||
if (rankCmp > 0) {
|
||||
return candidate;
|
||||
}
|
||||
if (rankCmp < 0) {
|
||||
return best;
|
||||
}
|
||||
return candidate.dayNumber < best.dayNumber ? candidate : best;
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
@@ -137,6 +137,15 @@ export interface OctaveConfig {
|
||||
* data/calendar/temporal-feasts/pentecost-sunday.yml), which is
|
||||
* stricter (`duplex`). */
|
||||
wins?: FeastClass;
|
||||
/** The octave's own effective rank on its *last* day specifically (the
|
||||
* "in Octava" closing day) instead of `wins` — live-verified as a real,
|
||||
* general pattern rather than a per-saint quirk: both St. Lawrence's
|
||||
* own Aug 17 and the Assumption's own Aug 22 show as Duplex, elevated
|
||||
* above their otherwise-Semiduplex ordinary in-between days. Default
|
||||
* 'duplex'. Feeds both the rival-saint threshold (calendar/octaves.ts's
|
||||
* `strictestThreshold`) and octave-vs-octave precedence
|
||||
* (`resolveActiveOctave`) on that one day. */
|
||||
closingDayRank?: FeastClass;
|
||||
}
|
||||
|
||||
export type DayWinner =
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Commemoration, DayWinner, LiturgicalDay } from '../calendar/types'
|
||||
import type { ProperText } from '../propers';
|
||||
import { getCommonProper, getTemporalProper } from '../propers';
|
||||
import { getSaintRecord } from '../calendar/feasts';
|
||||
import { activeOctavesFor, isAtLeast } from '../calendar';
|
||||
import { resolveActiveOctave, isAtLeast } from '../calendar';
|
||||
import { splitAntiphon } from './antiphon';
|
||||
|
||||
function toResolvedText(proper: ProperText): ResolvedText {
|
||||
@@ -60,8 +60,16 @@ export function resolveOfficeWinner(day: LiturgicalDay): DayWinner {
|
||||
if (day.temporalCategory !== 'ordinary-feria') {
|
||||
return day.winner;
|
||||
}
|
||||
for (const octave of activeOctavesFor(day.date)) {
|
||||
const saint = getSaintRecord(octave.id);
|
||||
// When more than one octave is active at once (St. Lawrence's and the
|
||||
// Assumption's genuinely overlap every Aug 16-17), resolveActiveOctave
|
||||
// picks the one that actually governs the day — see its own doc
|
||||
// comment for the precedence rule. Falls through to the plain temporal
|
||||
// default if that octave turns out to be a temporal-only one with no
|
||||
// saint record (e.g. Christmas's or Pentecost's own octave id) — no
|
||||
// sanctoral content to synthesize from those.
|
||||
const activeOctave = resolveActiveOctave(day.date);
|
||||
if (activeOctave) {
|
||||
const saint = getSaintRecord(activeOctave.id);
|
||||
if (saint) {
|
||||
return { kind: 'sanctoral', id: saint.id, name: saint.name, rank: saint.rank };
|
||||
}
|
||||
|
||||
@@ -62,6 +62,26 @@ describe('getDayLabel — active octave', () => {
|
||||
it("a season's own named anchor day outranks an active octave, even though Trinity Sunday is technically also day 8 of Pentecost's own octave", () => {
|
||||
expect(getDayLabel(resolveDay('2026-05-31'))).toBe('Trinity Sunday');
|
||||
});
|
||||
|
||||
it("a temporal day with real standing of its own (not ordinary-feria) never shows an octave name at all, even with one active -- live-verified counterexample: the Christmas Octave's own stack (Dec 30) titles itself off the Sunday, never any of the four octaves layered on top of it", () => {
|
||||
const day = resolveDay('2033-12-30');
|
||||
expect(day.temporalCategory).toBe('privileged-feria-minor');
|
||||
expect(getDayLabel(day)).not.toContain('Octave');
|
||||
expect(getDayLabel(day)).toBe('Friday in Christmastide');
|
||||
});
|
||||
|
||||
it("picks the higher-ranked of two genuinely overlapping octaves for the label -- St. Lawrence's own elevated closing day (Aug 17) over the Assumption's ordinary day 3, even though the Assumption's octave started later and the Assumption is the higher-ranked feast overall", () => {
|
||||
expect(getDayLabel(resolveDay('2026-08-17'))).toBe('8th Day within the Octave of St. Lawrence, Martyr');
|
||||
expect(getDayLabel(resolveDay('2026-08-18'))).toBe('4th Day within the Octave of The Assumption of the Blessed Virgin Mary');
|
||||
});
|
||||
|
||||
it('a real Sunday with standing of its own wins over both octaves entirely, showing the plain ordinal Sunday label -- not an octave name at all', () => {
|
||||
// 2026-08-16 is a Sunday genuinely within both St. Lawrence's and the
|
||||
// Assumption's overlapping octave windows.
|
||||
const day = resolveDay('2026-08-16');
|
||||
expect(day.weekday).toBe('sunday');
|
||||
expect(getDayLabel(day)).not.toContain('Octave');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDayLabel — feast name combination', () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { activeOctavesFor, strictestThreshold } from '../../src/calendar/octaves';
|
||||
import { activeOctavesFor, strictestThreshold, resolveActiveOctave, pickWinningOctave } from '../../src/calendar/octaves';
|
||||
import type { ActiveOctave } from '../../src/calendar/octaves';
|
||||
|
||||
describe('activeOctavesFor', () => {
|
||||
it('finds nothing outside any octave window', () => {
|
||||
@@ -11,10 +12,12 @@ describe('activeOctavesFor', () => {
|
||||
expect(day1).toEqual([{ id: 'st-lawrence', name: 'St. Lawrence, Martyr', wins: 'semiduplex', dayNumber: 1 }]);
|
||||
|
||||
// Aug 17 is also day 3 of the Assumption's own octave (started Aug 15) --
|
||||
// the two run concurrently this time of year.
|
||||
// the two run concurrently this time of year. Day 8 is Lawrence's own
|
||||
// closing day ("in Octava"), elevated to duplex -- live-verified real
|
||||
// pattern, not a per-saint quirk (see calendar/types.ts's OctaveConfig).
|
||||
const day8 = activeOctavesFor('2026-08-17');
|
||||
expect(day8).toEqual([
|
||||
{ id: 'st-lawrence', name: 'St. Lawrence, Martyr', wins: 'semiduplex', dayNumber: 8 },
|
||||
{ id: 'st-lawrence', name: 'St. Lawrence, Martyr', wins: 'duplex', dayNumber: 8 },
|
||||
{ id: 'assumption', name: 'The Assumption of the Blessed Virgin Mary', wins: 'semiduplex', dayNumber: 3 },
|
||||
]);
|
||||
|
||||
@@ -40,3 +43,39 @@ describe('activeOctavesFor', () => {
|
||||
expect(octaves.map((o) => o.dayNumber)).toEqual([5, 4, 3, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveActiveOctave / pickWinningOctave -- overlapping-octave precedence', () => {
|
||||
it('returns undefined when no octave is active, and the sole octave when only one is', () => {
|
||||
expect(resolveActiveOctave('2026-07-15')).toBeUndefined();
|
||||
expect(resolveActiveOctave('2026-08-10')?.id).toBe('st-lawrence');
|
||||
});
|
||||
|
||||
it("St. Lawrence's own elevated closing day (duplex) outranks the Assumption's ordinary day 3 (semiduplex) on their one real overlap, Aug 17", () => {
|
||||
const winner = resolveActiveOctave('2026-08-17');
|
||||
expect(winner?.id).toBe('st-lawrence');
|
||||
expect(winner?.wins).toBe('duplex');
|
||||
});
|
||||
|
||||
it("gives way to the Assumption once Lawrence's own octave has ended (Aug 18-22)", () => {
|
||||
expect(resolveActiveOctave('2026-08-18')?.id).toBe('assumption');
|
||||
});
|
||||
|
||||
// pickWinningOctave directly, since no real equal-rank overlap exists yet
|
||||
// in this app's own calendar to exercise the tie-break against live data.
|
||||
const higher = (dayNumber: number): ActiveOctave => ({ id: 'higher', name: 'Higher', wins: 'duplex', dayNumber });
|
||||
const lower = (dayNumber: number): ActiveOctave => ({ id: 'lower', name: 'Lower', wins: 'semiduplex', dayNumber });
|
||||
const equalA = (dayNumber: number): ActiveOctave => ({ id: 'equal-a', name: 'Equal A', wins: 'duplex', dayNumber });
|
||||
const equalB = (dayNumber: number): ActiveOctave => ({ id: 'equal-b', name: 'Equal B', wins: 'duplex', dayNumber });
|
||||
|
||||
it('picks the higher-ranked octave regardless of order or start date', () => {
|
||||
expect(pickWinningOctave([lower(2), higher(6)])?.id).toBe('higher');
|
||||
expect(pickWinningOctave([higher(6), lower(2)])?.id).toBe('higher');
|
||||
});
|
||||
|
||||
it('on a tied rank, picks the more recently started octave (smaller dayNumber) -- per direct instruction: day 1 of a newly started octave needs to be fully present, the whole point of it starting; the older, tied-rank octave loses and is commemorated instead', () => {
|
||||
// equalB started later (day 2, vs equalA's day 5 -- fewer days have
|
||||
// passed since it began) and should win the tie.
|
||||
expect(pickWinningOctave([equalA(5), equalB(2)])?.id).toBe('equal-b');
|
||||
expect(pickWinningOctave([equalB(2), equalA(5)])?.id).toBe('equal-b');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user