src/lib/dive-numbering.ts
Rama main · réplica de lectura del repositorio original.
import { DiveNumberingSummary } from "@/lib/api/dives";
import type { LogbookImportDive } from "@/lib/api/logbook-import";
import { combineStartTime, diveStartInstant } from "@/lib/date-time";
import { countNoun } from "@/lib/format";
// A plain-English description of what a renumber would tidy in a log, for the
// card above the dive list and the offer on an import's result. Null when
// nothing is unused, shared or out of date order, which takes both - and with
// them every way in to Renumber - off a tidy log entirely.
//
// Deliberately descriptive rather than corrective: it never says "should", and
// nothing here is phrased as a problem. Gaps are the ordinary shape of a log
// that continues a paper logbook, duplicates are what back-filling looks like
// halfway through, and only the diver knows which of theirs are deliberate. A
// line that scolds is a line they stop reading - including on the day it would
// have told them something they didn't know.
export function describeDiveNumbering(
summary: DiveNumberingSummary,
): string | null {
if (
summary.total_dives === 0 ||
summary.lowest === null ||
summary.highest === null
) {
return null;
}
const notes: string[] = [];
if (summary.missing_count > 0) {
notes.push(`${countNoun(summary.missing_count, "number")} unused`);
}
if (summary.duplicate_count > 0) {
notes.push(
`${countNoun(summary.duplicate_count, "dive shares", "dives share")} a number`,
);
}
if (summary.out_of_date_order_count > 0) {
notes.push(`${summary.out_of_date_order_count} out of date order`);
}
if (notes.length === 0) {
return null;
}
// `lowest === highest` survives the notes above only through duplicates -
// several dives on one number - so it is a range that collapsed, not a log
// of one dive, which has nothing for a renumber to change.
const range =
summary.lowest === summary.highest
? `#${summary.lowest}`
: `#${summary.lowest}–#${summary.highest}`;
return `Numbered ${range} — ${notes.join(", ")}.`;
}
// Whether a write left more dives sharing a number, or numbered out of date
// order, than the log had before it - what an import does when its dives take
// numbers the log already uses. Unused numbers are left out: the only ones an
// import adds are numbers its files state, which are the diver's own.
export function numberingTangledSince(
before: DiveNumberingSummary,
after: DiveNumberingSummary,
): boolean {
return (
after.duplicate_count > before.duplicate_count ||
after.out_of_date_order_count > before.out_of_date_order_count
);
}
// The renumber that offers to tidy after an import: from the day of the
// earliest dive it brought into the log, continuing from the dive before that
// day.
export interface ImportRenumberScope {
// `RenumberDivesDialog`'s "Only dives from" day.
fromDate: string;
// The last second before that day begins, for `getNextDiveNumber`: its answer
// is the number the renumber starts at.
lastInstantBefore: string;
}
const DAY_MS = 24 * 60 * 60 * 1000;
// Null when the import brought no dive into the log - every dive it reached
// was already there, or skipped.
//
// The day is read at `offsetMinutes`, which has to be the offset the dialog
// turns its date back into an instant with (the browser's), so that the scope
// it starts always reaches the earliest dive: a day read in the dive's own zone
// can begin after the dive does in the browser's.
export function importRenumberScope(
dives: readonly Pick<LogbookImportDive, "outcome" | "start_time">[],
offsetMinutes: number,
): ImportRenumberScope | null {
const instants = dives
.filter((dive) => dive.outcome === "new")
.map((dive) => dive.start_time)
.filter((start): start is string => start !== null)
.map(diveStartInstant)
.filter((instant) => !Number.isNaN(instant));
if (instants.length === 0) return null;
const offsetMs = offsetMinutes * 60_000;
const dayStart =
Math.floor((Math.min(...instants) + offsetMs) / DAY_MS) * DAY_MS;
const day = (ms: number) => new Date(ms).toISOString().slice(0, 10);
return {
fromDate: day(dayStart),
lastInstantBefore: combineStartTime(
`${day(dayStart - DAY_MS)} 23:59:59`,
offsetMinutes,
),
};
}