From d5a58efbbe9f3f41882a6e0c660e5a2d74dab17f Mon Sep 17 00:00:00 2001 From: Samuel Smith Date: Wed, 5 Nov 2025 01:16:29 -0500 Subject: [PATCH 1/7] strptime() parse fixes for some locales Fixes GH #86 --- Piece.xs | 85 +++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 54 insertions(+), 31 deletions(-) diff --git a/Piece.xs b/Piece.xs index 16c909a..0dd317f 100644 --- a/Piece.xs +++ b/Piece.xs @@ -645,30 +645,42 @@ label: AV* weekday_av = (AV*)SvRV(*weekday_sv); AV* wday_av = (AV*)SvRV(*wday_sv); + /* Use longest-match to handle ambiguous prefixes + (e.g., "Cuma" vs "Cumartesi" in Turkish) */ + int best_match = -1; + size_t best_len = 0; + for (i = 0; i <= av_len(weekday_av); i++) { - if (c == 'A') { - SV** day_sv = av_fetch(weekday_av, i, 0); - if (day_sv && SvPOK(*day_sv)) { - char* day_str = SvPV(*day_sv, len); - if (strncasecmp(buf, day_str, len) == 0) - break; + SV** day_sv; + + /* Try full weekday name */ + day_sv = av_fetch(weekday_av, i, 0); + if (day_sv && SvPOK(*day_sv)) { + char* day_str = SvPV(*day_sv, len); + if (len > best_len && strncasecmp(buf, day_str, len) == 0) { + best_match = i; + best_len = len; } - } else { - SV** day_sv = av_fetch(wday_av, i, 0); - if (day_sv && SvPOK(*day_sv)) { - char* day_str = SvPV(*day_sv, len); - if (strncasecmp(buf, day_str, len) == 0) - break; + } + + /* Try abbreviated weekday name */ + day_sv = av_fetch(wday_av, i, 0); + if (day_sv && SvPOK(*day_sv)) { + char* day_str = SvPV(*day_sv, len); + if (len > best_len && strncasecmp(buf, day_str, len) == 0) { + best_match = i; + best_len = len; } } } - if (i > av_len(weekday_av)) { + + if (best_match < 0) { warn("Failed parsing weekday names"); return NULL; } - tm->tm_wday = i; - buf += len; + tm->tm_wday = best_match; + buf += best_len; } break; @@ -758,31 +770,42 @@ label: AV* month_av = (AV*)SvRV(*month_sv); AV* mon_av = (AV*)SvRV(*mon_sv); - for (i = 0; i <= av_len(month_av); i++) { + /* Use longest-match to handle ambiguous prefixes + (e.g., "1" vs "10" in Japanese) */ + int best_match = -1; + size_t best_len = 0; - if (c == 'B') { - SV** month_sv_item = av_fetch(month_av, i, 0); - if (month_sv_item && SvPOK(*month_sv_item)) { - char* month_str = SvPV(*month_sv_item, len); - if (strncasecmp(buf, month_str, len) == 0) - break; + for (i = 0; i <= av_len(month_av); i++) { + SV** month_sv_item; + + /* Try full month name */ + month_sv_item = av_fetch(month_av, i, 0); + if (month_sv_item && SvPOK(*month_sv_item)) { + char* month_str = SvPV(*month_sv_item, len); + if (len > best_len && strncasecmp(buf, month_str, len) == 0) { + best_match = i; + best_len = len; } - } else { - SV** mon_sv_item = av_fetch(mon_av, i, 0); - if (mon_sv_item && SvPOK(*mon_sv_item)) { - char* mon_str = SvPV(*mon_sv_item, len); - if (strncasecmp(buf, mon_str, len) == 0) - break; + } + + /* Try abbreviated month name */ + month_sv_item = av_fetch(mon_av, i, 0); + if (month_sv_item && SvPOK(*month_sv_item)) { + char* month_str = SvPV(*month_sv_item, len); + if (len > best_len && strncasecmp(buf, month_str, len) == 0) { + best_match = i; + best_len = len; } } } - if (i > av_len(month_av)) { + + if (best_match < 0) { warn("Failed parsing month name"); return NULL; } - tm->tm_mon = i; - buf += len; + tm->tm_mon = best_match; + buf += best_len; } break; From dd4870ef1af066557af393d76f35d34c083f5c6c Mon Sep 17 00:00:00 2001 From: Samuel Smith Date: Fri, 7 Nov 2025 00:04:42 -0500 Subject: [PATCH 2/7] Add fullmon_list() and fullday_list() Fixes GH #85 --- Piece.pm | 48 +++++++++++++++++++++++++++++++++++++++++------- README.md | 28 +++++++++++++++++++++------- t/09locales.t | 47 +++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 107 insertions(+), 16 deletions(-) diff --git a/Piece.pm b/Piece.pm index 824b41b..43e74e4 100644 --- a/Piece.pm +++ b/Piece.pm @@ -677,6 +677,26 @@ sub mon_list { return @old; } +sub fullday_list { + shift if ref($_[0]) && $_[0]->isa(__PACKAGE__); # strip first if called as a method + my @old = @FULLDAY_LIST; + if (@_) { + @FULLDAY_LIST = @_; + &Time::Piece::_default_locale(); + } + return @old; +} + +sub fullmon_list { + shift if ref($_[0]) && $_[0]->isa(__PACKAGE__); # strip first if called as a method + my @old = @FULLMON_LIST; + if (@_) { + @FULLMON_LIST = @_; + &Time::Piece::_default_locale(); + } + return @old; +} + sub time_separator { shift if ref($_[0]) && $_[0]->isa(__PACKAGE__); my $old = $TIME_SEP; @@ -1087,10 +1107,12 @@ the actual offset including any DST adjustment. =head2 Global Configuration - $t->time_separator($s) # set the default separator (default ":") - $t->date_separator($s) # set the default separator (default "-") - $t->day_list(@days) # set the default weekdays - $t->mon_list(@days) # set the default months + $t->time_separator($s) # set the default separator (default ":") + $t->date_separator($s) # set the default separator (default "-") + $t->day_list(@days) # set the names used by wdayname() + $t->mon_list(@months) # set the names used by month() + $t->fullday_list(@days) # set the names used by fullday() + $t->fullmon_list(@months) # set the names used by fullmonth() =head2 Parsing @@ -1346,17 +1368,29 @@ B This is a global change affecting all Time::Piece instances. You can also override the day/month names manually: - my @days = qw( Domingo Lunes Martes Miercoles Jueves Viernes Sabado ); + # Abbreviated day names + my @days = qw( Dom Lun Mar Mie Jue Vie Sab ); my $spanish_day = localtime->day(@days); - my @months = qw( Enero Febrero Marzo Abril Mayo Junio - Julio Agosto Septiembre Octubre Noviembre Diciembre ); + # Full day names + my @fulldays = qw( Domingo Lunes Martes Miercoles Jueves Viernes Sabado ); + my $spanish_fullday = localtime->fullday(@fulldays); + + # Abbreviated month names + my @months = qw( Ene Feb Mar Abr May Jun Jul Ago Sep Oct Nov Dic ); print localtime->month(@months); + # Full month names + my @fullmonths = qw( Enero Febrero Marzo Abril Mayo Junio + Julio Agosto Septiembre Octubre Noviembre Diciembre ); + print localtime->fullmonth(@fullmonths); + Set globally with: Time::Piece::day_list(@days); Time::Piece::mon_list(@months); + Time::Piece::fullday_list(@fulldays); + Time::Piece::fullmon_list(@fullmonths); =head1 Global Overriding diff --git a/README.md b/README.md index 25dd7eb..eb397a8 100644 --- a/README.md +++ b/README.md @@ -121,10 +121,12 @@ the actual offset including any DST adjustment. ## Global Configuration - $t->time_separator($s) # set the default separator (default ":") - $t->date_separator($s) # set the default separator (default "-") - $t->day_list(@days) # set the default weekdays - $t->mon_list(@days) # set the default months + $t->time_separator($s) # set the default separator (default ":") + $t->date_separator($s) # set the default separator (default "-") + $t->day_list(@days) # set the names used by wdayname() + $t->mon_list(@months) # set the names used by month() + $t->fullday_list(@days) # set the names used by fullday() + $t->fullmon_list(@months) # set the names used by fullmonth() ## Parsing @@ -362,17 +364,29 @@ a list of your locale's day and month names: You can also override the day/month names manually: - my @days = qw( Domingo Lunes Martes Miercoles Jueves Viernes Sabado ); + # Abbreviated day names + my @days = qw( Dom Lun Mar Mie Jue Vie Sab ); my $spanish_day = localtime->day(@days); - my @months = qw( Enero Febrero Marzo Abril Mayo Junio - Julio Agosto Septiembre Octubre Noviembre Diciembre ); + # Full day names + my @fulldays = qw( Domingo Lunes Martes Miercoles Jueves Viernes Sabado ); + my $spanish_fullday = localtime->fullday(@fulldays); + + # Abbreviated month names + my @months = qw( Ene Feb Mar Abr May Jun Jul Ago Sep Oct Nov Dic ); print localtime->month(@months); + # Full month names + my @fullmonths = qw( Enero Febrero Marzo Abril Mayo Junio + Julio Agosto Septiembre Octubre Noviembre Diciembre ); + print localtime->fullmonth(@fullmonths); + Set globally with: Time::Piece::day_list(@days); Time::Piece::mon_list(@months); + Time::Piece::fullday_list(@fulldays); + Time::Piece::fullmon_list(@fullmonths); # Global Overriding diff --git a/t/09locales.t b/t/09locales.t index 7bafa35..3d5c297 100644 --- a/t/09locales.t +++ b/t/09locales.t @@ -29,8 +29,51 @@ $t->day_list(@frdays); cmp_ok( $t->day, 'eq', &Time::Piece::_locale()->{wday}[ $t->_wday ] ); cmp_ok( $t->fullday, 'eq', &Time::Piece::_locale()->{weekday}[ $t->_wday ] ); +# Test fullday_list() method +my @original_fulldays = $t->fullday_list(); +is( scalar(@original_fulldays), 7, 'fullday_list() returns 7 days' ); + +my @custom_fulldays = + qw( Domingo Lunes Martes Miercoles Jueves Viernes Sabado ); +$t->fullday_list(@custom_fulldays); +cmp_ok( + $t->fullday, 'eq', + &Time::Piece::_locale()->{weekday}[ $t->_wday ], + 'fullday() returns custom full day name from locale' +); +cmp_ok( + $t->fullday, 'eq', + $custom_fulldays[ $t->_wday ], + 'fullday() returns correct custom full day name' +); + +# Test fullmon_list() method +my @original_fullmons = $t->fullmon_list(); +is( scalar(@original_fullmons), 12, 'fullmon_list() returns 12 months' ); + +my @custom_fullmons = + qw( Enero Febrero Marzo Abril Mayo Junio Julio Agosto Septiembre Octubre Noviembre Diciembre ); +$t->fullmon_list(@custom_fullmons); +cmp_ok( + $t->fullmonth, 'eq', + &Time::Piece::_locale()->{month}[ $t->_mon ], + 'fullmonth() returns custom full month name from locale' +); +cmp_ok( + $t->fullmonth, 'eq', + $custom_fullmons[ $t->_mon ], + 'fullmonth() returns correct custom full month name' +); + +# Test strptime with custom full day and month names +# Using 2013-07-09 which is a Tuesday (Martes) in July (Julio) +my $parsed_both = $t->strptime( 'Martes, 9 Julio 2013', '%A, %d %B %Y' ); +cmp_ok( $parsed_both->_wday, '==', 2, 'strptime parses custom full day name' ); +cmp_ok( $parsed_both->_mon, '==', 6, 'strptime parses custom full month name' ); +cmp_ok( $parsed_both->mday, '==', 9, 'strptime parses day of month' ); +cmp_ok( $parsed_both->year, '==', 2013, 'strptime parses year' ); -#load local locale +#load local locale from system Time::Piece->use_locale(); #test reverse parsing @@ -122,4 +165,4 @@ for my $time ( } -done_testing(234); +done_testing(244); From b8b452e156fc487db285b4960dad2426ff080938 Mon Sep 17 00:00:00 2001 From: Samuel Smith Date: Fri, 7 Nov 2025 00:07:02 -0500 Subject: [PATCH 3/7] Add debug logging --- t/09locales.t | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/t/09locales.t b/t/09locales.t index 3d5c297..fc69077 100644 --- a/t/09locales.t +++ b/t/09locales.t @@ -129,7 +129,7 @@ for my $time ( eval { $parsed = $t->strptime( $t_str, $strp_format ); }; if ($@) { - warn("strptime failed with time $t_str and format $strp_format"); + warn("gmtime strptime failed with time $t_str and format $strp_format"); warn($@); next; } @@ -141,9 +141,9 @@ for my $time ( for my $time ( time(), # Now, whenever that might be - 1451606400, # 2016-01-01 00:00 - 1451653500, # 2016-01-01 13:05 - 1449014400, # 2015-12-02 00:00 + 1451606430, # 2016-01-01 00:00:30 + 1451653530, # 2016-01-01 13:05:30 + 1449014430, # 2015-12-02 00:00:30 ) { my $t = localtime($time); @@ -155,7 +155,7 @@ for my $time ( eval { $parsed = $t->strptime( $t_str, $strp_format ); }; if ($@) { - warn("strptime failed with time $t_str and format $strp_format"); + warn("local strptime failed with time $t_str and format $strp_format"); warn($@); next; } From 8e94ae476f3588621b1277d32bcec7ae569bda74 Mon Sep 17 00:00:00 2001 From: Samuel Smith Date: Fri, 7 Nov 2025 23:15:11 -0500 Subject: [PATCH 4/7] Fix null deref on invalid epoch Fixes GH #47 --- Piece.xs | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/Piece.xs b/Piece.xs index 0dd317f..038faec 100644 --- a/Piece.xs +++ b/Piece.xs @@ -317,6 +317,26 @@ my_mini_mktime(struct tm *ptm) ptm->tm_wday = (jday + WEEKDAY_BIAS) % 7; } +static struct tm +safe_localtime(pTHX_ const time_t *tp) +{ + struct tm *result = localtime(tp); + if (!result) { + croak("localtime failed for invalid time value"); + } + return *result; +} + +static struct tm +safe_gmtime(pTHX_ const time_t *tp) +{ + struct tm *result = gmtime(tp); + if (!result) { + croak("gmtime failed for invalid time value"); + } + return *result; +} + # if defined(WIN32) || (defined(__QNX__) && defined(__WATCOMC__)) # define strncasecmp(x,y,n) strnicmp(x,y,n) # endif @@ -848,7 +868,7 @@ label: buf = cp; Zero(&mytm, 1, struct tm); - mytm = *gmtime(&t); + mytm = safe_gmtime(aTHX_ &t); *got_GMT = 1; tm->tm_sec = mytm.tm_sec; @@ -1038,9 +1058,9 @@ _strftime(fmt, epoch, islocal = 1) size_t len; if(islocal == 1) - mytm = *localtime(&epoch); + mytm = safe_localtime(aTHX_ &epoch); else - mytm = *gmtime(&epoch); + mytm = safe_gmtime(aTHX_ &epoch); len = strftime(tmpbuf, TP_BUF_SIZE, fmt, &mytm); /* @@ -1163,7 +1183,7 @@ _strptime ( string, format, islocal, localization, defaults_ref ) if (got_GMT == 1 && islocal == 1) { time_t t; t = my_timegm(&mytm); - mytm = *localtime(&t); + mytm = safe_localtime(aTHX_ &t); } return_11part_tm(aTHX_ SP, &mytm); @@ -1176,7 +1196,7 @@ _mini_mktime(int sec, int min, int hour, int mday, int mon, int year) time_t t; PPCODE: t = 0; - mytm = *gmtime(&t); + mytm = safe_gmtime(aTHX_ &t); mytm.tm_sec = sec; mytm.tm_min = min; @@ -1195,8 +1215,8 @@ _crt_localtime(time_t sec) PREINIT: struct tm mytm; PPCODE: - if(ix) mytm = *gmtime(&sec); - else mytm = *localtime(&sec); + if(ix) mytm = safe_gmtime(aTHX_ &sec); + else mytm = safe_localtime(aTHX_ &sec); /* Need to get: $s,$n,$h,$d,$m,$y */ EXTEND(SP, 10); @@ -1226,7 +1246,7 @@ _get_localization() char buf[TP_BUF_SIZE]; size_t i; time_t t = 1325386800; /*1325386800 = Sun, 01 Jan 2012 03:00:00 GMT*/ - struct tm mytm = *gmtime(&t); + struct tm mytm = safe_gmtime(aTHX_ &t); CODE: for(i = 0; i < 7; ++i){ From 6f07798f822a6831ae4070021997335b7e34c831 Mon Sep 17 00:00:00 2001 From: Samuel Smith Date: Fri, 7 Nov 2025 23:40:53 -0500 Subject: [PATCH 5/7] Ignore locale-dependent format flags Fixes GH #71 --- Piece.pm | 25 ------------------------- Piece.xs | 20 -------------------- 2 files changed, 45 deletions(-) diff --git a/Piece.pm b/Piece.pm index 43e74e4..f3c0658 100644 --- a/Piece.pm +++ b/Piece.pm @@ -462,16 +462,6 @@ sub month_last_day { } my $strftime_trans_map = { - 'c' => sub { - my ( $format ) = @_; - if($LOCALE->{PM} && $LOCALE->{AM}){ - $format =~ s/%c/%a %d %b %Y %I:%M:%S %p/; - } - else{ - $format =~ s/%c/%a %d %b %Y %H:%M:%S/; - } - return $format; - }, 'e' => sub { my ( $format, $time ) = @_; my $day = sprintf( "%2d", $time->[c_mday] ); @@ -549,21 +539,6 @@ my $strftime_trans_map = { } return $format; }, - 'x' => sub { - my ( $format ) = @_; - $format =~ s/%x/%a %d %b %Y/; - return $format; - }, - 'X' => sub { - my ( $format ) = @_; - if($LOCALE->{PM} && $LOCALE->{AM}){ - $format =~ s/%X/%I:%M:%S %p/; - } - else{ - $format =~ s/%X/%H:%M:%S/; - } - return $format; - }, 'z' => sub { #%[zZ] not portable if time parts are from gmtime my ( $format, $time ) = @_; $format =~ s/%z/+0000/ if not $time->[c_islocal]; diff --git a/Piece.xs b/Piece.xs index 038faec..48f059e 100644 --- a/Piece.xs +++ b/Piece.xs @@ -444,14 +444,6 @@ label: tm->tm_year = i * 100 - 1900; break; - case 'c': - /* NOTE: c_fmt is intentionally ignored */ - - buf = _strptime(aTHX_ buf, "%a %d %b %Y %I:%M:%S %p %Z", tm, got_GMT, locales); - if (buf == 0) - return NULL; - break; - case 'D': buf = _strptime(aTHX_ buf, "%m/%d/%y", tm, got_GMT, locales); if (buf == 0) @@ -509,18 +501,6 @@ label: return NULL; break; - case 'X': - buf = _strptime(aTHX_ buf, "%I:%M:%S %p", tm, got_GMT, locales); - if (buf == 0) - return NULL; - break; - - case 'x': - buf = _strptime(aTHX_ buf, "%a %d %b %Y", tm, got_GMT, locales); - if (buf == 0) - return NULL; - break; - case 'j': if (!isDIGIT((unsigned char)*buf)) return NULL; From fbe42c859ed61193bfa3324e80c15e343056715f Mon Sep 17 00:00:00 2001 From: Samuel Smith Date: Sat, 8 Nov 2025 01:24:11 -0500 Subject: [PATCH 6/7] Document supported flags for strftime/strptime Fixes GH #83 --- Piece.pm | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/Piece.pm b/Piece.pm index f3c0658..f10494e 100644 --- a/Piece.pm +++ b/Piece.pm @@ -1043,6 +1043,24 @@ The following methods are available on the object: # of the full POSIX extension) $t->strftime() # "Tue, 29 Feb 2000 12:34:56 GMT" +=head3 strftime Format Flags + +The C method calls your system's native C implementation, +so the supported format flags and their behavior will depend on your platform. + +B Some format flags behave differently or may be missing +entirely on certain platforms. The following flags are known to have +platform-specific issues: C<%e>, C<%D>, C<%F>, C<%k>, C<%l>, C<%P>, C<%r>, C<%R>, +C<%s>, C<%T>, C<%u>, C<%V>, C<%z>, and C<%Z>. + +To mitigate these differences, C includes a special translation layer +that attempts to unify behavior across platforms. For example, C<%F> is not +available on some Microsoft platforms, so it is automatically converted to +C<"%Y-%m-%d"> internally before calling the system's C. + +For a complete list of format flags supported by your system, consult your +platform's C manual page (C on Unix-like systems). + =head2 Epoch and Calendar Calculations $t->epoch # seconds since the epoch @@ -1206,6 +1224,58 @@ The default format string is C<"%a, %d %b %Y %H:%M:%S %Z">, so these are equival my $t1 = Time::Piece->strptime($string); my $t2 = Time::Piece->strptime($string, "%a, %d %b %Y %H:%M:%S %Z"); +=head2 Supported Format Flags + +C uses a custom C implementation that supports the +following format flags: + + Flag Description + ---- ----------- + %% Literal '%' character + %a Abbreviated weekday name (Mon, Tue, etc.) + %A Full weekday name (Monday, Tuesday, etc.) + %b Abbreviated month name (Jan, Feb, etc.) + %B Full month name (January, February, etc.) + %C Century number (00-99) + %d Day of month (01-31) + %D Equivalent to %m/%d/%y + %e Day of month ( 1-31, space-padded) + %F Equivalent to %Y-%m-%d (ISO 8601 date format) + %h Abbreviated month name (same as %b) + %H Hour in 24-hour format (00-23) + %I Hour in 12-hour format (01-12) + %j Day of year (001-366) + %k Hour in 24-hour format ( 0-23, space-padded) + %l Hour in 12-hour format ( 1-12, space-padded) + %m Month number (01-12) + %M Minute (00-59) + %n Any whitespace + %p AM/PM indicator + %P Alt AM/PM indicator + %r Time in AM/PM format (%I:%M:%S %p, or %H:%M:%S if locale has no AM/PM) + %R Equivalent to %H:%M + %s Seconds since Unix epoch (1970-01-01 00:00:00 UTC) + %S Second (00-60, allowing for leap seconds) + %t Any whitespace (same as %n) + %T Equivalent to %H:%M:%S + %u Weekday as number (1-7, Monday = 1) + %w Weekday as number (0-6, Sunday = 0) + %y Year within century (00-99). Values 00-68 are 2000-2068, 69-99 are 1969-1999 + %Y Year with century (e.g., 2024) + %z Timezone offset (+HHMM, -HHMM, +HH:MM, or -HH:MM) + %Z Timezone name (only GMT and UTC recognized; others parsed but ignored) + +B The format flags C<%c>, C<%x>, and C<%X> are B +supported as they are highly locale-dependent and have inconsistent formats +across systems. However, you can construct equivalent formats using the individual +flags listed above. For example, C<%c> is typically equivalent to something like: + + "%a %b %e %H:%M:%S %Y" # e.g., "Tue Feb 29 12:34:56 2000" + +B C<%U>, C<%V>, and C<%W> (week number formats) are parsed but not fully +implemented in the strptime logic, as they require additional date components +to calculate the actual date. + =head2 GMT vs Local Time By default, C returns GMT objects when called as a class method: diff --git a/README.md b/README.md index eb397a8..4ad1d95 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,24 @@ The following methods are available on the object: # of the full POSIX extension) $t->strftime() # "Tue, 29 Feb 2000 12:34:56 GMT" +### strftime Format Flags + +The `strftime` method calls your system's native `strftime()` implementation, +so the supported format flags and their behavior will depend on your platform. + +**Platform Variability:** Some format flags behave differently or may be missing +entirely on certain platforms. The following flags are known to have +platform-specific issues: `%e`, `%D`, `%F`, `%k`, `%l`, `%P`, `%r`, `%R`, +`%s`, `%T`, `%u`, `%V`, `%z`, and `%Z`. + +To mitigate these differences, `Time::Piece` includes a special translation layer +that attempts to unify behavior across platforms. For example, `%F` is not +available on some Microsoft platforms, so it is automatically converted to +`"%Y-%m-%d"` internally before calling the system's `strftime()`. + +For a complete list of format flags supported by your system, consult your +platform's `strftime(3)` manual page (`man strftime` on Unix-like systems). + ## Epoch and Calendar Calculations $t->epoch # seconds since the epoch @@ -234,6 +252,58 @@ The default format string is `"%a, %d %b %Y %H:%M:%S %Z"`, so these are equivale my $t1 = Time::Piece->strptime($string); my $t2 = Time::Piece->strptime($string, "%a, %d %b %Y %H:%M:%S %Z"); +## Supported Format Flags + +`Time::Piece` uses a custom `strptime()` implementation that supports the +following format flags: + + Flag Description + ---- ----------- + %% Literal '%' character + %a Abbreviated weekday name (Mon, Tue, etc.) + %A Full weekday name (Monday, Tuesday, etc.) + %b Abbreviated month name (Jan, Feb, etc.) + %B Full month name (January, February, etc.) + %C Century number (00-99) + %d Day of month (01-31) + %D Equivalent to %m/%d/%y + %e Day of month ( 1-31, space-padded) + %F Equivalent to %Y-%m-%d (ISO 8601 date format) + %h Abbreviated month name (same as %b) + %H Hour in 24-hour format (00-23) + %I Hour in 12-hour format (01-12) + %j Day of year (001-366) + %k Hour in 24-hour format ( 0-23, space-padded) + %l Hour in 12-hour format ( 1-12, space-padded) + %m Month number (01-12) + %M Minute (00-59) + %n Any whitespace + %p AM/PM indicator + %P Alt AM/PM indicator + %r Time in AM/PM format (%I:%M:%S %p, or %H:%M:%S if locale has no AM/PM) + %R Equivalent to %H:%M + %s Seconds since Unix epoch (1970-01-01 00:00:00 UTC) + %S Second (00-60, allowing for leap seconds) + %t Any whitespace (same as %n) + %T Equivalent to %H:%M:%S + %u Weekday as number (1-7, Monday = 1) + %w Weekday as number (0-6, Sunday = 0) + %y Year within century (00-99). Values 00-68 are 2000-2068, 69-99 are 1969-1999 + %Y Year with century (e.g., 2024) + %z Timezone offset (+HHMM, -HHMM, +HH:MM, or -HH:MM) + %Z Timezone name (only GMT and UTC recognized; others parsed but ignored) + +**Unsupported Locale Flags:** The format flags `%c`, `%x`, and `%X` are **not** +supported as they are highly locale-dependent and have inconsistent formats +across systems. However, you can construct equivalent formats using the individual +flags listed above. For example, `%c` is typically equivalent to something like: + + "%a %b %e %H:%M:%S %Y" # e.g., "Tue Feb 29 12:34:56 2000" + +**Note:** `%U`, `%V`, and `%W` (week number formats) are parsed but not fully +implemented in the strptime logic, as they require additional date components +to calculate the actual date. + ## GMT vs Local Time By default, `strptime` returns GMT objects when called as a class method: From 4cbe7a324ce3a70990bda7c66309d817696a9a9a Mon Sep 17 00:00:00 2001 From: Samuel Smith Date: Sat, 8 Nov 2025 01:26:55 -0500 Subject: [PATCH 7/7] Version bump --- Changes | 7 +++++++ Piece.pm | 2 +- Seconds.pm | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Changes b/Changes index 5a13dd5..714884a 100644 --- a/Changes +++ b/Changes @@ -1,5 +1,12 @@ Revision history for Time-Piece +1.40 2025-11-08 + - strptime: locale parse fixes (GH86) + - Add fullmon_list() and fullday_list() (GH85) + - Fix null deref (GH47) + - Remove locale-dependent flags (GH71) + - Document flags for strftime/strptime (GH83) + 1.39 2025-10-24 - strptime: allow year < 1900 (GH56) - add add_days() (GH65) diff --git a/Piece.pm b/Piece.pm index f10494e..cfa53a4 100644 --- a/Piece.pm +++ b/Piece.pm @@ -19,7 +19,7 @@ our %EXPORT_TAGS = ( ':override' => 'internal', ); -our $VERSION = '1.39'; +our $VERSION = '1.40'; XSLoader::load( 'Time::Piece', $VERSION ); diff --git a/Seconds.pm b/Seconds.pm index ba6f1a9..fe27e48 100644 --- a/Seconds.pm +++ b/Seconds.pm @@ -1,7 +1,7 @@ package Time::Seconds; use strict; -our $VERSION = '1.39'; +our $VERSION = '1.40'; use Exporter 5.57 'import';