feat(dates): calendar-day date filters in the owner's display zone (#152)

/photos, /photos/exif and unified search take date_from_day /
date_to_day (YYYY-MM-DD): the server turns them into the first day's
start and the last day's end in the owner's display zone, the same days
/memories groups by. Each replaces its instant counterpart. Clients stop
computing midnights in the device's zone, which drifted by the travel
offset whenever the phone's zone differed from display_tz.

e2e check: every fixture is included by its display-zone day and
excluded by the days either side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cameron CordesandClaude Opus 5.5 authored and cameron committed 2026-10-17 03:24:50 +00:00
1 parent a11d20bdcd
commit bfc3c1c21a
5 files changed
+133 -5

No files matched your search

+74 -2
View File
@@ -121,13 +121,39 @@ pub fn day_start_instant(day: NaiveDate, zone: Tz) -> i64 {
/// exact call in Rust without a row being dropped first.
pub const LEGACY_INSTANT_BAND_SECS: i64 = 15 * 3600;
/// Fold calendar-day filter params (`date_from_day` / `date_to_day`,
/// `YYYY-MM-DD`) into the instant bounds: the first day's start and the
/// last day's end in the owner's display zone, the same days `/memories`
/// groups by (#152). A day param replaces its instant counterpart; an
/// unparseable one is ignored. `zone` is only called when a day is given,
/// so requests without them never read prefs.
pub fn apply_day_bounds(
from: &mut Option<i64>,
to: &mut Option<i64>,
from_day: Option<&str>,
to_day: Option<&str>,
zone: impl FnOnce() -> Tz,
) {
if from_day.is_none() && to_day.is_none() {
return;
}
let zone = zone();
let parse = |d: &str| NaiveDate::parse_from_str(d.trim(), "%Y-%m-%d").ok();
if let Some(day) = from_day.and_then(parse) {
*from = Some(day_start_instant(day, zone));
}
if let Some(next) = to_day.and_then(parse).and_then(|d| d.succ_opt()) {
*to = Some(day_start_instant(next, zone) - 1);
}
}
/// An inclusive true-instant range (`date_from` / `date_to` on the photo
/// listing endpoints), plus the zones [`row_true_instant`] needs to read
/// unconverted rows. Either bound may be open.
///
/// Bounds are instants, never wall clocks: a client filtering by calendar
/// day sends that day's midnights in the owner's display zone (see
/// `nl_query::iso_to_unix`), so the range agrees with
/// day sends `date_from_day` / `date_to_day` and the handler turns them into
/// display-zone day bounds ([`apply_day_bounds`]), so the range agrees with
/// [`row_display_day`] grouping by construction.
#[derive(Clone, Copy, Debug)]
pub struct InstantRange {
@@ -698,6 +724,52 @@ mod tests {
);
}
#[test]
fn day_bounds_are_the_display_zone_day() {
// July 5 is a different stretch of instants for a NY and a Tokyo
// owner; the device's zone plays no part (#152).
let (mut from, mut to) = (None, None);
apply_day_bounds(
&mut from,
&mut to,
Some("2024-07-05"),
Some("2024-07-05"),
ny,
);
assert_eq!((from, to), (Some(1_720_152_000), Some(1_720_238_399)));
let (mut from, mut to) = (None, None);
apply_day_bounds(
&mut from,
&mut to,
Some("2024-07-05"),
Some("2024-07-05"),
|| Tz::Asia__Tokyo,
);
assert_eq!((from, to), (Some(1_720_105_200), Some(1_720_191_599)));
// A 23-hour DST day still ends at the next midnight.
let (mut from, mut to) = (None, None);
apply_day_bounds(
&mut from,
&mut to,
Some("2024-03-10"),
Some("2024-03-10"),
ny,
);
assert_eq!((from, to), (Some(1_710_046_800), Some(1_710_129_599)));
}
#[test]
fn day_bounds_replace_their_side_only_and_skip_bad_input() {
let (mut from, mut to) = (Some(1), Some(2));
apply_day_bounds(&mut from, &mut to, None, Some("2024-07-05"), ny);
assert_eq!((from, to), (Some(1), Some(1_720_238_399)));
let (mut from, mut to) = (Some(1), Some(2));
apply_day_bounds(&mut from, &mut to, Some("July 5th"), None, ny);
assert_eq!((from, to), (Some(1), Some(2)));
// No day params: the zone (a prefs read) is never asked for.
apply_day_bounds(&mut from, &mut to, None, None, || panic!("zone read"));
}
#[test]
fn display_day_respects_a_non_new_york_display_zone() {
// Owner in Tokyo: 2024-07-05 01:30 UTC = 10:30 JST July 5 (not