Nobody had any comments, so I've pushed it to trunk now.

On Thu, 26 Sept 2024 at 13:57, Jonathan Wakely <jwak...@redhat.com> wrote:
>
> Does this rounding heuristic seem reasonable? I have discussed it with
> Howard and he agreed that rounding "2024-09-22 18:34:56" up to the next
> day, 2024-09-23, could be surprising. But he convinced me that using
> chrono::round was correct in general (certainly better than what we have
> now using chrono::time_point_cast). A period like ratio<1,60> (e.g.
> a 60Hz refresh rate) can't be represented exactly with decimal digits,
> so we want to round from the parsed value to the closest representation
> in the result type. Truncating is strictly inferior when we're dealing
> with such periods.
>
> The hybrid rounding approach here tries to Do The Right Thing.
>
> MSVC seems to avoid this problem by just refusing to parse the date time
> "2024-09-22 18:34:56" into a result type of sys_days, presumably because
> the parsed value cannot be represented in the result type. I can see
> some justification for that, but I think rounding is more useful than
> failing to parse.
>
> Tested x86_64-linux.
>
> -- >8 --
>
> I noticed that chrono::parse was using duration_cast and time_point_cast
> to convert the parsed value to the result. Those functions truncate
> towards zero, which is not generally what you want. Especially for
> negative times before the epoch, where truncating towards zero rounds
> "up" towards the next duration/time_point. Using chrono::round is
> typically better, as that rounds to nearest.
>
> However, while testing the fix I realised that rounding to the nearest
> can give surprising results in some cases. For example if we parse a
> chrono::sys_days using chrono::parse("F %T", "2024-09-22 18:34:56", tp)
> then we will round up to the next day, i.e. sys_days(2024y/09/23). That
> seems surprising, and I think 2024-09-22 is what most users would
> expect.
>
> This change attempts to provide a hybrid rounding heuristic where we use
> chrono::round for the general case, but when the result has a period
> that is one of minutes, hours, days, weeks, or years then we truncate
> towards negative infinity using chrono::floor. This means that we
> truncate "2024-09-22 18:34:56" to the start of the current
> minute/hour/day/week/year, instead of rounding up to 2024-09-23, or to
> 18:35, or 17:00. For a period of months chrono::round is used, because
> the months duration is defined as a twelfth of a year, which is not
> actually the length of any calendar month. We don't want to truncate to
> a whole number of "months" if that can actually go from e.g. 2023-03-01
> to 2023-01-31, because February is shorter than chrono::months(1).
>
> libstdc++-v3/ChangeLog:
>
>         * include/bits/chrono_io.h (__detail::__use_floor): New
>         function.
>         (__detail::__round): New function.
>         (from_stream): Use __detail::__round.
>         * testsuite/std/time/clock/file/io.cc: Check for expected
>         rounding in parse.
>         * testsuite/std/time/clock/gps/io.cc: Likewise.
> ---
>  libstdc++-v3/include/bits/chrono_io.h         | 64 +++++++++++++++++--
>  .../testsuite/std/time/clock/file/io.cc       | 21 +++++-
>  .../testsuite/std/time/clock/gps/io.cc        | 20 ++++++
>  3 files changed, 97 insertions(+), 8 deletions(-)
>
> diff --git a/libstdc++-v3/include/bits/chrono_io.h 
> b/libstdc++-v3/include/bits/chrono_io.h
> index 1e34c82b532..362bb5aa9e9 100644
> --- a/libstdc++-v3/include/bits/chrono_io.h
> +++ b/libstdc++-v3/include/bits/chrono_io.h
> @@ -2407,6 +2407,56 @@ namespace __detail
>    template<typename _Duration>
>      using _Parser_t = _Parser<common_type_t<_Duration, seconds>>;
>
> +  template<typename _Duration>
> +    consteval bool
> +    __use_floor()
> +    {
> +      if constexpr (_Duration::period::den == 1)
> +       {
> +         switch (_Duration::period::num)
> +         {
> +           case minutes::period::num:
> +           case hours::period::num:
> +           case days::period::num:
> +           case weeks::period::num:
> +           case years::period::num:
> +             return true;
> +         }
> +       }
> +      return false;
> +    }
> +
> +  // A "do the right thing" rounding function for duration and time_point
> +  // values extracted by from_stream. When treat_as_floating_point is true
> +  // we don't want to do anything, just a straightforward conversion.
> +  // When the destination type has a period of minutes, hours, days, weeks,
> +  // or years, we use chrono::floor to truncate towards negative infinity.
> +  // This ensures that an extracted timestamp such as 2024-09-05 13:00:00
> +  // will produce 2024-09-05 when rounded to days, rather than rounding up
> +  // to 2024-09-06 (a different day).
> +  // Otherwise, use chrono::round to get the nearest value representable
> +  // in the destination type.
> +  template<typename _ToDur, typename _Tp>
> +    constexpr auto
> +    __round(const _Tp& __t)
> +    {
> +      if constexpr (__is_duration_v<_Tp>)
> +       {
> +         if constexpr (treat_as_floating_point_v<typename _Tp::rep>)
> +           return chrono::duration_cast<_ToDur>(__t);
> +         else if constexpr (__detail::__use_floor<_ToDur>())
> +           return chrono::floor<_ToDur>(__t);
> +         else
> +           return chrono::round<_ToDur>(__t);
> +       }
> +      else
> +       {
> +         static_assert(__is_time_point_v<_Tp>);
> +         using _Tpt = time_point<typename _Tp::clock, _ToDur>;
> +         return _Tpt(__detail::__round<_ToDur>(__t.time_since_epoch()));
> +       }
> +    }
> +
>  } // namespace __detail
>  /// @endcond
>
> @@ -2421,7 +2471,7 @@ namespace __detail
>        auto __need = __format::_ChronoParts::_TimeOfDay;
>        __detail::_Parser_t<duration<_Rep, _Period>> __p(__need);
>        if (__p(__is, __fmt, __abbrev, __offset))
> -       __d = chrono::duration_cast<duration<_Rep, _Period>>(__p._M_time);
> +       __d = __detail::__round<duration<_Rep, _Period>>(__p._M_time);
>        return __is;
>      }
>
> @@ -2882,7 +2932,7 @@ namespace __detail
>           else
>             {
>               auto __st = __p._M_sys_days + __p._M_time - *__offset;
> -             __tp = chrono::time_point_cast<_Duration>(__st);
> +             __tp = __detail::__round<_Duration>(__st);
>             }
>         }
>        return __is;
> @@ -2918,7 +2968,7 @@ namespace __detail
>           // "23:59:60" to correctly produce a time within a leap second.
>           auto __ut = utc_clock::from_sys(__p._M_sys_days) + __p._M_time
>                         - *__offset;
> -         __tp = chrono::time_point_cast<_Duration>(__ut);
> +         __tp = __detail::__round<_Duration>(__ut);
>         }
>        return __is;
>      }
> @@ -2956,7 +3006,7 @@ namespace __detail
>               constexpr sys_days __epoch(-days(4383)); // 1958y/1/1
>               auto __d = __p._M_sys_days - __epoch + __p._M_time - *__offset;
>               tai_time<common_type_t<_Duration, seconds>> __tt(__d);
> -             __tp = chrono::time_point_cast<_Duration>(__tt);
> +             __tp = __detail::__round<_Duration>(__tt);
>             }
>         }
>        return __is;
> @@ -2995,7 +3045,7 @@ namespace __detail
>               constexpr sys_days __epoch(days(3657)); // 1980y/1/Sunday[1]
>               auto __d = __p._M_sys_days - __epoch + __p._M_time - *__offset;
>               gps_time<common_type_t<_Duration, seconds>> __gt(__d);
> -             __tp = chrono::time_point_cast<_Duration>(__gt);
> +             __tp = __detail::__round<_Duration>(__gt);
>             }
>         }
>        return __is;
> @@ -3020,7 +3070,7 @@ namespace __detail
>      {
>        sys_time<_Duration> __st;
>        if (chrono::from_stream(__is, __fmt, __st, __abbrev, __offset))
> -       __tp = chrono::time_point_cast<_Duration>(file_clock::from_sys(__st));
> +       __tp = __detail::__round<_Duration>(file_clock::from_sys(__st));
>        return __is;
>      }
>
> @@ -3049,7 +3099,7 @@ namespace __detail
>         {
>           days __d = __p._M_sys_days.time_since_epoch();
>           auto __t = local_days(__d) + __p._M_time; // ignore offset
> -         __tp = chrono::time_point_cast<_Duration>(__t);
> +         __tp = __detail::__round<_Duration>(__t);
>         }
>        return __is;
>      }
> diff --git a/libstdc++-v3/testsuite/std/time/clock/file/io.cc 
> b/libstdc++-v3/testsuite/std/time/clock/file/io.cc
> index 9da5019ab78..27b4507236e 100644
> --- a/libstdc++-v3/testsuite/std/time/clock/file/io.cc
> +++ b/libstdc++-v3/testsuite/std/time/clock/file/io.cc
> @@ -67,8 +67,27 @@ test_parse()
>    VERIFY( abbrev == "456" );
>    VERIFY( offset == (1h + 23min) );
>
> -  ss.str("");
> +  // Test rounding
>    ss.clear();
> +  ss.str("2224-09-06 23");
> +  file_time<days> d;
> +  ss >> parse("%F %H", d); // Should be truncated to start of day, not 
> rounded.
> +  ss.str("");
> +  ss << d;
> +  VERIFY( ss.str() == "2224-09-06 00:00:00" );
> +  ss.str("1969-12-31 23");
> +  ss >> parse("%F %H", d); // Should be truncated to start of day, not 
> rounded.
> +  ss.str("");
> +  ss << d;
> +  VERIFY( ss.str() == "1969-12-31 00:00:00" );
> +
> +  file_time<duration<long, std::ratio<10>>> ds; // decaseconds
> +  ss.str("2224-09-06 15:07:06");
> +  ss >> parse("%F %T", ds); // Should be rounded to nearest decasecond.
> +  ss << ds;
> +  VERIFY( ss.str() == "2224-09-06 15:07:10" );
> +
> +  ss.str("");
>    ss << file_time<seconds>{};
>    VERIFY( ss >> parse("%F %T", tp) );
>    VERIFY( tp.time_since_epoch() == 0s );
> diff --git a/libstdc++-v3/testsuite/std/time/clock/gps/io.cc 
> b/libstdc++-v3/testsuite/std/time/clock/gps/io.cc
> index c012520080a..647ed021b24 100644
> --- a/libstdc++-v3/testsuite/std/time/clock/gps/io.cc
> +++ b/libstdc++-v3/testsuite/std/time/clock/gps/io.cc
> @@ -75,6 +75,26 @@ test_parse()
>    VERIFY( abbrev == "GPS" );
>    VERIFY( offset == -(12h + 34min) );
>
> +  // Test rounding
> +  ss.clear();
> +  ss.str("2224-09-06 23");
> +  gps_time<days> d;
> +  ss >> parse("%F %H", d); // Should be truncated to start of day, not 
> rounded.
> +  ss.str("");
> +  ss << d;
> +  VERIFY( ss.str() == "2224-09-06 00:00:00" );
> +  ss.str("1969-12-31 23");
> +  ss >> parse("%F %H", d); // Should be truncated to start of day, not 
> rounded.
> +  ss.str("");
> +  ss << d;
> +  VERIFY( ss.str() == "1969-12-31 00:00:00" );
> +
> +  gps_time<duration<long, std::ratio<10>>> ds; // decaseconds
> +  ss.str("2224-09-06 15:07:06");
> +  ss >> parse("%F %T", ds); // Should be rounded to nearest decasecond.
> +  ss << ds;
> +  VERIFY( ss.str() == "2224-09-06 15:07:10" );
> +
>    ss.str("");
>    ss << gps_seconds{};
>    VERIFY( ss >> parse("%F %T", tp) );
> --
> 2.46.1
>

Reply via email to