fix: correct timezone offset for parsed dates, add timezone_local option

This commit is contained in:
ThetaDev 2025-01-25 01:13:38 +01:00
parent 3a2370b97c
commit a5a7be5b4e
No known key found for this signature in database
GPG key ID: E319D3C5148D65B6
15 changed files with 202 additions and 98 deletions

View file

@ -220,7 +220,6 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
let mut mapper = response::YouTubeListMapper::<VideoItem>::with_channel(
ctx.lang,
ctx.utc_offset,
&channel_data.c,
channel_data.warnings,
);
@ -266,7 +265,6 @@ impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
let mut mapper = response::YouTubeListMapper::<PlaylistItem>::with_channel(
ctx.lang,
ctx.utc_offset,
&channel_data.c,
channel_data.warnings,
);

View file

@ -177,10 +177,11 @@ impl MapResponse<Paginator<HistoryItem<VideoItem>>> for response::History {
for item in items.c {
match item {
response::YouTubeListItem::ItemSectionRenderer { header, contents } => {
let mut mapper = YouTubeListMapper::<VideoItem>::new(ctx.lang, ctx.utc_offset);
let mut mapper = YouTubeListMapper::<VideoItem>::new(ctx.lang);
mapper.map_response(contents);
mapper.conv_history_items(
header.map(|h| h.item_section_header_renderer.title),
ctx.utc_offset,
&mut map_res,
);
}
@ -228,7 +229,7 @@ impl MapResponse<Paginator<VideoItem>> for response::History {
.section_list_renderer
.contents;
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(ctx.lang, ctx.utc_offset);
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(ctx.lang);
mapper.map_response(items);
Ok(MapResult {

View file

@ -897,8 +897,6 @@ impl RustyPipeBuilder {
/// Set the timezone and its associated UTC offset in minutes used
/// when accessing the YouTube API.
///
/// This will also change the UTC offset of the returned dates.
///
/// **Default value**: `0` (UTC)
///
/// **Info**: you can set this option for individual queries, too
@ -909,6 +907,16 @@ impl RustyPipeBuilder {
self
}
/// Access the YouTube API using the local system timezone
///
/// If the local timezone could not be determined, an error is logged and RustyPipe falls
/// back to UTC.
#[must_use]
pub fn timezone_local(self) -> Self {
let (timezone, utc_offset_minutes) = local_tz_offset();
self.timezone(timezone, utc_offset_minutes)
}
/// Generate a report on every operation.
///
/// This should only be used for debugging.
@ -1689,8 +1697,6 @@ impl RustyPipeQuery {
/// Set the timezone and its associated UTC offset in minutes used
/// when accessing the YouTube API.
///
/// This will also change the UTC offset of the returned dates.
#[must_use]
pub fn timezone<S: Into<String>>(mut self, timezone: S, utc_offset_minutes: i16) -> Self {
self.opts.timezone = Some(timezone.into());
@ -1698,6 +1704,13 @@ impl RustyPipeQuery {
self
}
/// Access the YouTube API using the local system timezone
#[must_use]
pub fn timezone_local(self) -> Self {
let (timezone, utc_offset_minutes) = local_tz_offset();
self.timezone(timezone, utc_offset_minutes)
}
/// Generate a report on every operation.
///
/// This should only be used for debugging.
@ -2611,6 +2624,19 @@ fn validate_country(country: Country) -> Country {
}
}
fn local_tz_offset() -> (String, i16) {
match (
util::local_timezone_name(),
UtcOffset::current_local_offset().map_err(|_| Error::Other("indeterminate offset".into())),
) {
(Ok(timezone), Ok(offset)) => (timezone, offset.whole_minutes()),
(Err(e), _) | (_, Err(e)) => {
tracing::error!("{e}");
("UTC".to_owned(), 0)
}
}
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -160,7 +160,7 @@ impl MapResponse<Paginator<HistoryItem<TrackItem>>> for response::MusicHistory {
};
let mut mapper = MusicListMapper::new(ctx.lang);
mapper.map_response(shelf.contents);
mapper.conv_history_items(shelf.title, &mut map_res);
mapper.conv_history_items(shelf.title, ctx.utc_offset, &mut map_res);
}
let ctoken = contents

View file

@ -127,7 +127,7 @@ impl MapResponse<Paginator<YouTubeItem>> for response::Continuation {
let estimated_results = self.estimated_results;
let items = continuation_items(self);
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(ctx.lang, ctx.utc_offset);
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(ctx.lang);
mapper.map_response(items);
Ok(MapResult {
@ -237,11 +237,11 @@ impl MapResponse<Paginator<HistoryItem<VideoItem>>> for response::Continuation {
for item in items.c {
match item {
response::YouTubeListItem::ItemSectionRenderer { header, contents } => {
let mut mapper =
response::YouTubeListMapper::<VideoItem>::new(ctx.lang, ctx.utc_offset);
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(ctx.lang);
mapper.map_response(contents);
mapper.conv_history_items(
header.map(|h| h.item_section_header_renderer.title),
ctx.utc_offset,
&mut map_res,
);
}
@ -281,7 +281,7 @@ impl MapResponse<Paginator<HistoryItem<TrackItem>>> for response::MusicContinuat
let mut map_shelf = |shelf: response::music_item::MusicShelf| {
let mut mapper = MusicListMapper::new(ctx.lang);
mapper.map_response(shelf.contents);
mapper.conv_history_items(shelf.title, &mut map_res);
mapper.conv_history_items(shelf.title, ctx.utc_offset, &mut map_res);
continuations.extend(shelf.continuations);
};

View file

@ -90,7 +90,7 @@ impl MapResponse<Playlist> for response::Playlist {
.playlist_video_list_renderer
.contents;
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(ctx.lang, ctx.utc_offset);
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(ctx.lang);
mapper.map_response(video_items);
let (description, thumbnails, last_update_txt) = match self.sidebar {

View file

@ -1,5 +1,6 @@
use serde::Deserialize;
use serde_with::{rust::deserialize_ignore_any, serde_as, DefaultOnError, VecSkipError};
use time::UtcOffset;
use crate::{
model::{
@ -1272,6 +1273,7 @@ impl MusicListMapper {
pub fn conv_history_items(
self,
date_txt: Option<String>,
utc_offset: UtcOffset,
res: &mut MapResult<Vec<HistoryItem<TrackItem>>>,
) {
res.warnings.extend(self.warnings);
@ -1282,7 +1284,12 @@ impl MusicListMapper {
.map(|item| HistoryItem {
item,
playback_date: date_txt.as_deref().and_then(|s| {
timeago::parse_textual_date_to_d(self.lang, s, &mut res.warnings)
timeago::parse_textual_date_to_d(
self.lang,
utc_offset,
s,
&mut res.warnings,
)
}),
playback_date_txt: date_txt.clone(),
}),

View file

@ -461,7 +461,6 @@ impl IsShort for Vec<TimeOverlay> {
#[derive(Debug)]
pub(crate) struct YouTubeListMapper<T> {
lang: Language,
utc_offset: UtcOffset,
channel: Option<ChannelTag>,
pub items: Vec<T>,
@ -471,10 +470,9 @@ pub(crate) struct YouTubeListMapper<T> {
}
impl<T> YouTubeListMapper<T> {
pub fn new(lang: Language, utc_offset: UtcOffset) -> Self {
pub fn new(lang: Language) -> Self {
Self {
lang,
utc_offset,
channel: None,
items: Vec::new(),
warnings: Vec::new(),
@ -483,15 +481,9 @@ impl<T> YouTubeListMapper<T> {
}
}
pub fn with_channel<C>(
lang: Language,
utc_offset: UtcOffset,
channel: &Channel<C>,
warnings: Vec<String>,
) -> Self {
pub fn with_channel<C>(lang: Language, channel: &Channel<C>, warnings: Vec<String>) -> Self {
Self {
lang,
utc_offset,
channel: Some(ChannelTag {
id: channel.id.clone(),
name: channel.name.clone(),
@ -794,12 +786,7 @@ impl<T> YouTubeListMapper<T> {
thumbnail: tn.image.into(),
channel,
publish_date: publish_date_txt.as_deref().and_then(|t| {
timeago::parse_textual_date_or_warn(
self.lang,
self.utc_offset,
t,
&mut self.warnings,
)
timeago::parse_timeago_dt_or_warn(self.lang, t, &mut self.warnings)
}),
publish_date_txt,
view_count,
@ -920,17 +907,16 @@ impl YouTubeListMapper<VideoItem> {
pub(crate) fn conv_history_items(
self,
date_txt: Option<String>,
utc_offset: UtcOffset,
res: &mut MapResult<Vec<HistoryItem<VideoItem>>>,
) {
res.warnings.extend(self.warnings);
res.c.extend(self.items.into_iter().map(|item| {
HistoryItem {
item,
playback_date: date_txt.as_deref().and_then(|s| {
timeago::parse_textual_date_to_d(self.lang, s, &mut res.warnings)
}),
playback_date_txt: date_txt.clone(),
}
res.c.extend(self.items.into_iter().map(|item| HistoryItem {
item,
playback_date: date_txt.as_deref().and_then(|s| {
timeago::parse_textual_date_to_d(self.lang, utc_offset, s, &mut res.warnings)
}),
playback_date_txt: date_txt.clone(),
}));
}
}

View file

@ -107,7 +107,7 @@ impl<T: FromYtItem> MapResponse<SearchResult<T>> for response::Search {
.section_list_renderer
.contents;
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(ctx.lang, ctx.utc_offset);
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(ctx.lang);
mapper.map_response(items);
Ok(MapResult {

View file

@ -45,7 +45,7 @@ impl MapResponse<Vec<VideoItem>> for response::Trending {
.section_list_renderer
.contents;
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(ctx.lang, ctx.utc_offset);
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(ctx.lang);
mapper.map_response(items);
Ok(MapResult {

View file

@ -475,7 +475,7 @@ fn map_recommendations(
visitor_data: Option<String>,
ctx: &MapRespCtx<'_>,
) -> MapResult<Paginator<VideoItem>> {
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(ctx.lang, ctx.utc_offset);
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(ctx.lang);
mapper.map_response(r);
mapper.ctoken = mapper.ctoken.or_else(|| {

View file

@ -1,5 +1,7 @@
use time::{Date, Duration, Month, OffsetDateTime};
use crate::error::Error;
/// Shift a date by the given number of months.
/// Ambiguous month-ends are shifted backwards as necessary.
pub fn shift_months(date: Date, months: i32) -> Date {
@ -25,7 +27,8 @@ pub fn shift_years(date: Date, years: i32) -> Date {
shift_months(date, years * 12)
}
pub fn shift_weeks_mo(date: Date, weeks: i32) -> Date {
/// Shift a date to the monday of its week, plus/minus the given amount of weeks
pub fn shift_weeks_monday(date: Date, weeks: i32) -> Date {
let d = date + Duration::weeks(weeks.into());
Date::from_iso_week_date(d.year(), d.iso_week(), time::Weekday::Monday).unwrap()
}
@ -40,3 +43,75 @@ pub fn now_sec() -> OffsetDateTime {
.replace_nanosecond(0)
.unwrap()
}
/// Gets the current timezone from the system.
///
/// Currently only supported for Windows, Unix, and WASM targets.
///
/// # Errors
/// Returns an [Error](enum@Error) if the timezone cannot be determined.
pub fn local_timezone_name() -> Result<String, Error> {
#[cfg(unix)]
{
use std::path::Path;
let path = Path::new("/etc/localtime");
let realpath = std::fs::read_link(path)
.map_err(|_| Error::Other("could not read localtime".into()))?;
// The part of the path we're interested in cannot contain non unicode characters.
return realpath
.to_str()
.and_then(|s| s.split("/zoneinfo/").last())
.map(str::to_owned)
.ok_or_else(|| {
Error::Other(format!("could not parse zoneinfo path: {realpath:?}").into())
});
}
#[cfg(windows)]
#[allow(unsafe_code)]
{
unsafe {
use windows_sys::Win32::System::Time::GetDynamicTimeZoneInformation;
use windows_sys::Win32::System::Time::DYNAMIC_TIME_ZONE_INFORMATION;
let mut data: DYNAMIC_TIME_ZONE_INFORMATION = std::mem::zeroed();
let res = GetDynamicTimeZoneInformation(&mut data as _);
if res > 2 {
return Err(Error::Other("local timezone could not be read".into()));
} else {
let win_name_utf16 = &data.TimeZoneKeyName;
let mut len: usize = 0;
while win_name_utf16[len] != 0x0 {
len += 1;
}
if len == 0 {
return Err(Error::Other("local timezone could not be read".into()));
}
return String::from_utf16(&win_name_utf16[..len])
.map_err(|_| "local timezone is invalid UTF16".into())?;
}
}
}
#[allow(unreachable_code)]
Err(Error::Other("local timezone unsupported".into()))
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use time::{macros::date, Date};
#[rstest]
#[case::this_week(date!(2025-01-17), 0, date!(2025-01-13))]
#[case::last_week(date!(2025-01-17), -1, date!(2025-01-06))]
#[case::last_month(date!(2025-01-17), -4, date!(2024-12-16))]
fn shift_weeks_monday(#[case] date: Date, #[case] weeks: i32, #[case] expect: Date) {
let res = super::shift_weeks_monday(date, weeks);
assert_eq!(res, expect);
}
#[test]
fn local_timezone_name() {
super::local_timezone_name().unwrap();
}
}

View file

@ -5,7 +5,7 @@ mod visitor_data;
pub mod dictionary;
pub mod timeago;
pub use date::{now_sec, shift_months, shift_weeks_mo, shift_years};
pub use date::{local_timezone_name, now_sec, shift_months, shift_weeks_monday, shift_years};
pub use protobuf::{string_from_pb, ProtoBuilder};
pub use visitor_data::VisitorDataCache;

View file

@ -97,6 +97,26 @@ impl TimeAgo {
fn secs(self) -> u32 {
u32::from(self.n) * self.unit.secs()
}
fn into_datetime(self, utc_offset: UtcOffset) -> OffsetDateTime {
let ts = util::now_sec().to_offset(utc_offset);
match self.unit {
TimeUnit::Month => ts.replace_date(util::shift_months(ts.date(), -i32::from(self.n))),
TimeUnit::Year => ts.replace_date(util::shift_years(ts.date(), -i32::from(self.n))),
TimeUnit::LastWeek => {
ts.replace_date(util::shift_weeks_monday(ts.date(), -i32::from(self.n)))
}
TimeUnit::LastWeekday => ts.replace_date(
Date::from_iso_week_date(
ts.year(),
ts.iso_week(),
time::Weekday::Monday.nth_next(self.n),
)
.unwrap(),
),
_ => ts - Duration::from(self),
}
}
}
impl Mul<u8> for TimeAgo {
@ -116,33 +136,11 @@ impl From<TimeAgo> for Duration {
}
}
impl From<TimeAgo> for OffsetDateTime {
fn from(ta: TimeAgo) -> Self {
let ts = util::now_sec();
match ta.unit {
TimeUnit::Month => ts.replace_date(util::shift_months(ts.date(), -i32::from(ta.n))),
TimeUnit::Year => ts.replace_date(util::shift_years(ts.date(), -i32::from(ta.n))),
TimeUnit::LastWeek => {
ts.replace_date(util::shift_weeks_mo(ts.date(), -i32::from(ta.n)))
}
TimeUnit::LastWeekday => ts.replace_date(
Date::from_iso_week_date(
ts.year(),
ts.iso_week(),
time::Weekday::Monday.nth_next(ta.n),
)
.unwrap(),
),
_ => ts - Duration::from(ta),
}
}
}
impl From<ParsedDate> for OffsetDateTime {
fn from(date: ParsedDate) -> Self {
match date {
ParsedDate::Absolute(date) => date.with_hms(0, 0, 0).unwrap().assume_utc(),
ParsedDate::Relative(timeago) => timeago.into(),
impl ParsedDate {
fn into_datetime(self, utc_offset: UtcOffset) -> OffsetDateTime {
match self {
ParsedDate::Absolute(date) => date.with_hms(0, 0, 0).unwrap().assume_offset(utc_offset),
ParsedDate::Relative(timeago) => timeago.into_datetime(utc_offset),
}
}
}
@ -247,7 +245,7 @@ pub fn parse_timeago(lang: Language, textual_date: &str) -> Option<TimeAgo> {
///
/// Returns [`None`] if the date could not be parsed.
pub fn parse_timeago_dt(lang: Language, textual_date: &str) -> Option<OffsetDateTime> {
parse_timeago(lang, textual_date).map(OffsetDateTime::from)
parse_timeago(lang, textual_date).map(|t| t.into_datetime(UtcOffset::UTC))
}
pub fn parse_timeago_dt_or_warn(
@ -265,7 +263,11 @@ pub fn parse_timeago_dt_or_warn(
/// Parse a textual date (e.g. "29 minutes ago" or "Jul 2, 2014") into a ParsedDate object.
///
/// Returns [`None`] if the date could not be parsed.
pub fn parse_textual_date(lang: Language, textual_date: &str) -> Option<ParsedDate> {
pub fn parse_textual_date(
lang: Language,
utc_offset: UtcOffset,
textual_date: &str,
) -> Option<ParsedDate> {
let entry = dictionary::entry(lang);
let by_char = util::lang_by_char(lang);
let filtered_str = filter_datestr(textual_date);
@ -317,8 +319,9 @@ pub fn parse_textual_date(lang: Language, textual_date: &str) -> Option<ParsedDa
.ok()
.and_then(|m| {
Date::from_calendar_date(
y.map(i32::from)
.unwrap_or_else(|| OffsetDateTime::now_utc().year()),
y.map(i32::from).unwrap_or_else(|| {
OffsetDateTime::now_utc().to_offset(utc_offset).year()
}),
m,
d.unwrap_or(1) as u8,
)
@ -338,8 +341,7 @@ pub fn parse_textual_date_to_dt(
utc_offset: UtcOffset,
textual_date: &str,
) -> Option<OffsetDateTime> {
parse_textual_date(lang, textual_date)
.map(|parsed| OffsetDateTime::from(parsed).replace_offset(utc_offset))
parse_textual_date(lang, utc_offset, textual_date).map(|t| t.into_datetime(utc_offset))
}
/// Parse a textual date (e.g. "29 minutes ago" "Jul 2, 2014") into a Date object.
@ -347,11 +349,12 @@ pub fn parse_textual_date_to_dt(
/// Returns None if the date could not be parsed.
pub fn parse_textual_date_to_d(
lang: Language,
utc_offset: UtcOffset,
textual_date: &str,
warnings: &mut Vec<String>,
) -> Option<Date> {
parse_textual_date_or_warn(lang, UtcOffset::UTC, textual_date, warnings)
.map(OffsetDateTime::date)
parse_textual_date_or_warn(lang, utc_offset, textual_date, warnings)
.map(|d| d.to_offset(utc_offset).date())
}
pub fn parse_textual_date_or_warn(
@ -871,7 +874,7 @@ mod tests {
for (t, entry) in entries {
entry.cases.iter().for_each(|(txt, n)| {
let timeago = parse_timeago(*lang, txt);
let textual_date = parse_textual_date(*lang, txt);
let textual_date = parse_textual_date(*lang, UtcOffset::UTC, txt);
assert_eq!(
timeago,
Some(TimeAgo { n: *n, unit: *t }),
@ -913,7 +916,7 @@ mod tests {
#[case] textual_date: &str,
#[case] expect: Option<ParsedDate>,
) {
let parsed_date = parse_textual_date(lang, textual_date);
let parsed_date = parse_textual_date(lang, UtcOffset::UTC, textual_date);
assert_eq!(parsed_date, expect);
}
@ -924,7 +927,7 @@ mod tests {
#[case] textual_date: &str,
#[case] expect: Date,
) {
let parsed_date = parse_textual_date(lang, textual_date);
let parsed_date = parse_textual_date(lang, UtcOffset::UTC, textual_date);
let expected_date = expect
.replace_year(OffsetDateTime::now_utc().year())
.unwrap();
@ -940,7 +943,7 @@ mod tests {
for (lang, samples) in &date_samples {
assert_eq!(
parse_textual_date(*lang, samples.get("Today").unwrap()),
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Today").unwrap()),
Some(ParsedDate::Relative(TimeAgo {
n: 0,
unit: TimeUnit::Day
@ -948,7 +951,7 @@ mod tests {
"lang: {lang}"
);
assert_eq!(
parse_textual_date(*lang, samples.get("Yesterday").unwrap()),
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Yesterday").unwrap()),
Some(ParsedDate::Relative(TimeAgo {
n: 1,
unit: TimeUnit::Day
@ -956,7 +959,7 @@ mod tests {
"lang: {lang}"
);
assert_eq!(
parse_textual_date(*lang, samples.get("Ago").unwrap()),
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Ago").unwrap()),
Some(ParsedDate::Relative(TimeAgo {
n: 5,
unit: TimeUnit::Day
@ -964,62 +967,62 @@ mod tests {
"lang: {lang}"
);
assert_eq!(
parse_textual_date(*lang, samples.get("Jan").unwrap()),
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Jan").unwrap()),
Some(ParsedDate::Absolute(date!(2020 - 1 - 3))),
"lang: {lang}"
);
assert_eq!(
parse_textual_date(*lang, samples.get("Feb").unwrap()),
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Feb").unwrap()),
Some(ParsedDate::Absolute(date!(2016 - 2 - 7))),
"lang: {lang}"
);
assert_eq!(
parse_textual_date(*lang, samples.get("Mar").unwrap()),
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Mar").unwrap()),
Some(ParsedDate::Absolute(date!(2015 - 3 - 9))),
"lang: {lang}"
);
assert_eq!(
parse_textual_date(*lang, samples.get("Apr").unwrap()),
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Apr").unwrap()),
Some(ParsedDate::Absolute(date!(2017 - 4 - 2))),
"lang: {lang}"
);
assert_eq!(
parse_textual_date(*lang, samples.get("May").unwrap()),
parse_textual_date(*lang, UtcOffset::UTC, samples.get("May").unwrap()),
Some(ParsedDate::Absolute(date!(2014 - 5 - 22))),
"lang: {lang}"
);
assert_eq!(
parse_textual_date(*lang, samples.get("Jun").unwrap()),
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Jun").unwrap()),
Some(ParsedDate::Absolute(date!(2014 - 6 - 28))),
"lang: {lang}"
);
assert_eq!(
parse_textual_date(*lang, samples.get("Jul").unwrap()),
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Jul").unwrap()),
Some(ParsedDate::Absolute(date!(2014 - 7 - 2))),
"lang: {lang}"
);
assert_eq!(
parse_textual_date(*lang, samples.get("Aug").unwrap()),
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Aug").unwrap()),
Some(ParsedDate::Absolute(date!(2015 - 8 - 23))),
"lang: {lang}"
);
assert_eq!(
parse_textual_date(*lang, samples.get("Sep").unwrap()),
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Sep").unwrap()),
Some(ParsedDate::Absolute(date!(2018 - 9 - 16))),
"lang: {lang}"
);
assert_eq!(
parse_textual_date(*lang, samples.get("Oct").unwrap()),
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Oct").unwrap()),
Some(ParsedDate::Absolute(date!(2014 - 10 - 31))),
"lang: {lang}"
);
assert_eq!(
parse_textual_date(*lang, samples.get("Nov").unwrap()),
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Nov").unwrap()),
Some(ParsedDate::Absolute(date!(2016 - 11 - 3))),
"lang: {lang}"
);
assert_eq!(
parse_textual_date(*lang, samples.get("Dec").unwrap()),
parse_textual_date(*lang, UtcOffset::UTC, samples.get("Dec").unwrap()),
Some(ParsedDate::Absolute(date!(2021 - 12 - 24))),
"lang: {lang}"
);
@ -1065,7 +1068,7 @@ mod tests {
}
};
assert_eq!(
parse_textual_date(lang, &v),
parse_textual_date(lang, UtcOffset::UTC, &v),
Some(expected),
"lang={lang}; {k}"
);