Refactor utils::format_timespan to use chrono types (#9610)

This also adjusts the periods for years and months. Previously, we were using values of 30 days for a month and 12 months for year. Now, we use the chrono values of a month as 1/12 of a year and a year as 365.2425 days.
This commit is contained in:
Charles Dang 2024-11-28 22:07:52 -05:00 committed by GitHub
parent 70a76b0e21
commit 2c216eec72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 185 additions and 191 deletions

View file

@ -0,0 +1,115 @@
/*
Copyright (C) 2024
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include "formula/string_utils.hpp"
#include "serialization/chrono.hpp"
#include <array>
#include <chrono>
#include <string>
#include <vector>
namespace utils
{
namespace implementation
{
static constexpr std::array descriptors {
// TRANSLATORS: The "timespan^$num xxxxx" strings originating from the same file
// as the string with this comment MUST be translated following the usual rules
// for WML variable interpolation -- that is, without including or translating
// the caret^ prefix, and leaving the $num variable specification intact, since
// it is technically code. The only translatable natural word to be found here
// is the time unit (year, month, etc.) For example, for French you would
// translate "timespan^$num years" as "$num ans", thus allowing the game UI to
// generate output such as "39 ans" after variable interpolation.
std::tuple{ N_n("timespan^$num year", "timespan^$num years") },
std::tuple{ N_n("timespan^$num month", "timespan^$num months") },
std::tuple{ N_n("timespan^$num week", "timespan^$num weeks") },
std::tuple{ N_n("timespan^$num day", "timespan^$num days") },
std::tuple{ N_n("timespan^$num hour", "timespan^$num hours") },
std::tuple{ N_n("timespan^$num minute", "timespan^$num minutes") },
std::tuple{ N_n("timespan^$num second", "timespan^$num seconds") },
};
// Each duration type should have its description at its matching descriptor index
static constexpr auto deconstruct_format = std::tuple<
chrono::years,
chrono::months,
chrono::weeks,
chrono::days,
std::chrono::hours,
std::chrono::minutes,
std::chrono::seconds
>{};
} // namespace implementation
/**
* Formats a timespan into human-readable text for player authentication functions.
*
* This is generally meant for player-facing text rather than lightweight tasks like
* debug logging. The resulting output may differ based on current language settings.
*
* This is intentionally not a very thorough representation of time intervals.
* See <https://github.com/wesnoth/wesnoth/issues/6036> for more information.
*
* @param span The timespan to format
* @param detailed Whether to display more specific values such as "3 months, 2 days,
* 30 minutes, and 1 second". If not specified or set to @a false, the
* return value will ONLY include most significant time unit (e.g. "3
* months").
* @return A human-readable timespan description.
*
* @note The implementation formats the given timespan according to periods defined by
* the C++ chrono standard. As such, a year is defined as its average Gregorian
* length of 365.2425 days, while months are exactly 1/12 of a year. Furthermore,
* it doesn't take into account leap years or leap seconds. If you need to
* account for those, you are better off importing a new library and providing it
* with more specific information about the start and end times of the interval;
* otherwise your next best option is to hire a fortune teller to manually service
* your requests every time instead of this function.
*/
template<typename Rep, typename Period>
static std::string format_timespan(const std::chrono::duration<Rep, Period>& span, bool detailed = false)
{
if(span.count() <= 0) {
return _("timespan^expired");
}
std::vector<t_string> display_text;
const auto push_description = [&](const auto& time_component, const auto& description)
{
auto amount = time_component.count();
if(amount <= 0) {
return true; // Continue to next element
}
const auto& [fmt_singular, fmt_plural] = description;
display_text.emplace_back(VNGETTEXT(fmt_singular, fmt_plural, amount, {{"num", std::to_string(amount)}}));
return detailed;
};
std::apply(
[&push_description](auto&&... args) {
std::size_t i{0};
(... && push_description(args, implementation::descriptors[i++]));
},
chrono::deconstruct_duration(implementation::deconstruct_format, span));
return format_conjunct_list(_("timespan^expired"), display_text);
}
} // namespace utils

View file

@ -293,52 +293,6 @@ std::string format_disjunct_list(const t_string& empty, const std::vector<t_stri
return VGETTEXT("disjunct end^$prefix, or $last", {{"prefix", prefix}, {"last", elems.back()}});
}
std::string format_timespan(std::time_t time, bool detailed)
{
if(time <= 0) {
return _("timespan^expired");
}
typedef std::tuple<std::time_t, const char*, const char*> time_factor;
static const std::vector<time_factor> TIME_FACTORS{
// TRANSLATORS: The "timespan^$num xxxxx" strings originating from the same file
// as the string with this comment MUST be translated following the usual rules
// for WML variable interpolation -- that is, without including or translating
// the caret^ prefix, and leaving the $num variable specification intact, since
// it is technically code. The only translatable natural word to be found here
// is the time unit (year, month, etc.) For example, for French you would
// translate "timespan^$num years" as "$num ans", thus allowing the game UI to
// generate output such as "39 ans" after variable interpolation.
time_factor{ 31104000, N_n("timespan^$num year", "timespan^$num years") }, // 12 months
time_factor{ 2592000, N_n("timespan^$num month", "timespan^$num months") }, // 30 days
time_factor{ 604800, N_n("timespan^$num week", "timespan^$num weeks") },
time_factor{ 86400, N_n("timespan^$num day", "timespan^$num days") },
time_factor{ 3600, N_n("timespan^$num hour", "timespan^$num hours") },
time_factor{ 60, N_n("timespan^$num minute", "timespan^$num minutes") },
time_factor{ 1, N_n("timespan^$num second", "timespan^$num seconds") },
};
std::vector<t_string> display_text;
string_map i18n;
for(const auto& factor : TIME_FACTORS) {
const auto [ secs, fmt_singular, fmt_plural ] = factor;
const int amount = time / secs;
if(amount) {
time -= secs * amount;
i18n["num"] = std::to_string(amount);
display_text.emplace_back(VNGETTEXT(fmt_singular, fmt_plural, amount, i18n));
if(!detailed) {
break;
}
}
}
return format_conjunct_list(_("timespan^expired"), display_text);
}
}
std::string vgettext_impl(const char *domain

View file

@ -78,27 +78,6 @@ std::string format_conjunct_list(const t_string& empty, const std::vector<t_stri
*/
std::string format_disjunct_list(const t_string& empty, const std::vector<t_string>& elems);
/**
* Formats a timespan into human-readable text for player authentication functions.
*
* This is intentionally not a very thorough representation of time intervals.
* See <https://github.com/wesnoth/wesnoth/issues/6036> for more information.
*
* @param time The timespan in seconds.
* @param detailed Whether to display more specific values such as "3 months, 2 days,
* 30 minutes, and 1 second". If not specified or set to @a false, the
* return value will ONLY include most significant time unit (e.g. "3
* months").
* @return A human-readable timespan description.
*
* @note The implementation is not very precise because not all months in the Gregorian
* calendar have 30 days. Furthermore, it doesn't take into account leap years or
* leap seconds. If you need to account for those, you are better off importing
* a new library and providing it with more specific information about the start and
* end times of the interval; otherwise your next best option is to hire a fortune
* teller to manually service your requests every time instead of this function.
*/
std::string format_timespan(std::time_t time, bool detailed = false);
}
/**

View file

@ -19,6 +19,7 @@
#include "commandline_options.hpp"
#include "connect_engine.hpp"
#include "events.hpp"
#include "formula/format_timespan.hpp"
#include "formula/string_utils.hpp"
#include "game_config_manager.hpp"
#include "game_initialization/playcampaign.hpp"
@ -396,7 +397,8 @@ std::unique_ptr<wesnothd_connection> mp_manager::open_connection(std::string hos
const auto extra_data = error->optional_child("data");
if(extra_data) {
i18n_symbols["duration"] = utils::format_timespan((*extra_data)["duration"].to_time_t());
using namespace std::chrono_literals;
i18n_symbols["duration"] = utils::format_timespan(chrono::parse_duration((*extra_data)["duration"], 0s));
}
const std::string ec = (*error)["error_code"];

View file

@ -407,7 +407,7 @@ std::string format_timespan(const std::chrono::seconds& span)
return "expired";
}
auto [days, hours, minutes, seconds] = chrono::deconstruct_duration(span);
auto [days, hours, minutes, seconds] = chrono::deconstruct_duration(chrono::format::days_hours_mins_secs, span);
std::vector<std::string> formatted_values;
// TODO C++20: see if we can use the duration stream operators

View file

@ -72,15 +72,27 @@ inline auto parse_duration(const config_attribute_value& val, const Duration& de
return Duration{val.to_long_long(def.count())};
}
template<typename Rep, typename Period>
constexpr auto deconstruct_duration(const std::chrono::duration<Rep, Period>& span)
template<typename... Ts, typename Rep, typename Period>
constexpr auto deconstruct_duration(const std::tuple<Ts...>&, const std::chrono::duration<Rep, Period>& span)
{
auto days = std::chrono::duration_cast<chrono::days>(span);
auto hours = std::chrono::duration_cast<std::chrono::hours>(span - days);
auto minutes = std::chrono::duration_cast<std::chrono::minutes>(span - days - hours);
auto seconds = std::chrono::duration_cast<std::chrono::seconds>(span - days - hours - minutes);
return std::tuple{ days, hours, minutes, seconds };
auto time_remaining = std::chrono::duration_cast<std::common_type_t<Ts...>>(span);
return std::tuple{[&time_remaining]() {
auto duration = std::chrono::duration_cast<Ts>(time_remaining);
time_remaining -= duration;
return duration;
}()...};
}
/** Helper types to be used with @ref deconstruct_duration */
namespace format
{
constexpr auto days_hours_mins_secs = std::tuple<
chrono::days,
std::chrono::hours,
std::chrono::minutes,
std::chrono::seconds
>{};
} // namespace format
} // namespace chrono

View file

@ -152,7 +152,7 @@ std::ostream& metrics::requests(std::ostream& out) const
std::ostream& operator<<(std::ostream& out, metrics& met)
{
const auto time_up = std::chrono::steady_clock::now() - met.started_at_;
auto [days, hours, minutes, seconds] = chrono::deconstruct_duration(time_up);
auto [days, hours, minutes, seconds] = chrono::deconstruct_duration(chrono::format::days_hours_mins_secs, time_up);
const int requests_immediate = met.nrequests_ - met.nrequests_waited_;
const int percent_immediate = (requests_immediate*100)/(met.nrequests_ > 0 ? met.nrequests_ : 1);

View file

@ -15,132 +15,64 @@
#include <boost/test/unit_test.hpp>
#include "formula/string_utils.hpp"
#include "tstring.hpp"
#include <algorithm>
#include "formula/format_timespan.hpp"
#include "serialization/chrono.hpp"
BOOST_AUTO_TEST_SUITE( formula_timespan )
using std::time_t;
namespace {
enum TIME_FACTORS
{
YEAR = 31104000, /* 12 months */
MONTH = 2592000, /* 30 days */
WEEK = 604800,
DAY = 86400,
HOUR = 3600,
MIN = 60,
SEC = 1,
};
inline std::string minifmt(time_t t, const std::string& singular, const std::string& plural)
{
return t ? std::to_string(t) + " " + (t > 1 ? plural : singular) : "";
}
typedef std::tuple<
time_t /*sec*/,
time_t /*min*/,
time_t /*hr*/,
time_t /*day*/,
time_t /*wk*/,
time_t /*mo*/,
time_t /*yr*/> time_detailed;
inline time_t gen_as_time_t(const time_detailed& params)
{
auto [sec, min, hr, day, wk, mo, yr] = params;
return YEAR*yr + MONTH*mo + WEEK*wk + DAY*day + HOUR*hr + MIN*min + SEC*sec;
}
inline std::string gen_as_str(const time_detailed& params)
{
auto [sec, min, hr, day, wk, mo, yr] = params;
std::vector<t_string> bits;
std::string res;
bits.emplace_back(minifmt(yr, "year", "years"));
bits.emplace_back(minifmt(mo, "month", "months"));
bits.emplace_back(minifmt(wk, "week", "weeks"));
bits.emplace_back(minifmt(day, "day", "days"));
bits.emplace_back(minifmt(hr, "hour", "hours"));
bits.emplace_back(minifmt(min, "minute", "minutes"));
bits.emplace_back(minifmt(sec, "second", "seconds"));
// Drop zeroes
auto p = std::remove_if(bits.begin(), bits.end(), [](const t_string& t) { return t.empty(); });
if(p != bits.end()) {
bits.erase(p);
}
return utils::format_conjunct_list("expired", bits);
}
inline void test_format_timespan(const time_detailed& tcase, const std::string& detailed, const std::string& fuzzy="")
{
BOOST_CHECK_EQUAL(detailed, utils::format_timespan(gen_as_time_t(tcase), true));
if(!fuzzy.empty()) {
BOOST_REQUIRE_NE(detailed, fuzzy); // ensure test case params are not borked
BOOST_CHECK_EQUAL(fuzzy, utils::format_timespan(gen_as_time_t(tcase)));
BOOST_CHECK_NE(utils::format_timespan(gen_as_time_t(tcase)), utils::format_timespan(gen_as_time_t(tcase), true));
}
}
}
BOOST_AUTO_TEST_CASE( test_formula_timespan )
{
test_format_timespan({ 1, 0, 0, 0, 0, 0, 0 }, "1 second");
using namespace std::chrono_literals;
test_format_timespan({ 2, 0, 0, 0, 0, 0, 0 }, "2 seconds");
BOOST_CHECK_EQUAL("1 second", utils::format_timespan(std::chrono::seconds{1}, true));
BOOST_CHECK_EQUAL("2 seconds", utils::format_timespan(std::chrono::seconds{2}, true));
BOOST_CHECK_EQUAL("1 minute", utils::format_timespan(std::chrono::minutes{1}, true));
BOOST_CHECK_EQUAL("2 minutes", utils::format_timespan(std::chrono::minutes{2}, true));
BOOST_CHECK_EQUAL("1 hour", utils::format_timespan(std::chrono::hours{1}, true));
BOOST_CHECK_EQUAL("2 hours", utils::format_timespan(std::chrono::hours{2}, true));
BOOST_CHECK_EQUAL("1 day", utils::format_timespan(chrono::days{1}, true));
BOOST_CHECK_EQUAL("2 days", utils::format_timespan(chrono::days{2}, true));
BOOST_CHECK_EQUAL("1 week", utils::format_timespan(chrono::weeks{1}, true));
BOOST_CHECK_EQUAL("2 weeks", utils::format_timespan(chrono::weeks{2}, true));
BOOST_CHECK_EQUAL("1 year", utils::format_timespan(chrono::years{1}, true));
BOOST_CHECK_EQUAL("2 years", utils::format_timespan(chrono::years{2}, true));
test_format_timespan({ 0, 1, 0, 0, 0, 0, 0 }, "1 minute");
BOOST_CHECK_EQUAL(utils::format_timespan(0s), utils::format_timespan(0min));
BOOST_CHECK_EQUAL(utils::format_timespan(0s, true), utils::format_timespan(0min));
BOOST_CHECK_EQUAL(utils::format_timespan(0s), utils::format_timespan(-10000min));
BOOST_CHECK_EQUAL(utils::format_timespan(0s, true), utils::format_timespan(-10000min));
test_format_timespan({ 0, 2, 0, 0, 0, 0, 0 }, "2 minutes");
{
constexpr auto time = chrono::years{2} + chrono::months{5} + chrono::weeks{2} + chrono::days{3} +
std::chrono::hours{23} + std::chrono::minutes{1} + std::chrono::seconds{12};
test_format_timespan({ 0, 0, 1, 0, 0, 0, 0 }, "1 hour");
BOOST_CHECK_EQUAL("2 years, 5 months, 2 weeks, 3 days, 23 hours, 1 minute, and 12 seconds", utils::format_timespan(time, true));
BOOST_CHECK_EQUAL("2 years", utils::format_timespan(time));
}
test_format_timespan({ 0, 0, 2, 0, 0, 0, 0 }, "2 hours");
{
constexpr auto time = chrono::days{2} + std::chrono::hours{1} + std::chrono::seconds{4};
BOOST_CHECK_EQUAL("2 days, 1 hour, and 4 seconds", utils::format_timespan(time, true));
BOOST_CHECK_EQUAL("2 days", utils::format_timespan(time));
}
test_format_timespan({ 0, 0, 0, 1, 0, 0, 0 }, "1 day");
{
constexpr auto time = chrono::years{4} + chrono::weeks{2} + chrono::days{4} + std::chrono::minutes{40};
BOOST_CHECK_EQUAL("4 years, 2 weeks, 4 days, and 40 minutes", utils::format_timespan(time, true));
BOOST_CHECK_EQUAL("4 years", utils::format_timespan(time));
}
test_format_timespan({ 0, 0, 0, 2, 0, 0, 0 }, "2 days");
{
constexpr auto time = chrono::years{4} + chrono::months{3} + std::chrono::hours{1};
BOOST_CHECK_EQUAL("4 years, 3 months, and 1 hour", utils::format_timespan(time, true));
BOOST_CHECK_EQUAL("4 years", utils::format_timespan(time));
}
test_format_timespan({ 0, 0, 0, 0, 1, 0, 0 }, "1 week");
test_format_timespan({ 0, 0, 0, 0, 2, 0, 0 }, "2 weeks");
test_format_timespan({ 0, 0, 0, 0, 0, 1, 0 }, "1 month");
test_format_timespan({ 0, 0, 0, 0, 0, 2, 0 }, "2 months");
test_format_timespan({ 0, 0, 0, 0, 0, 0, 1 }, "1 year");
test_format_timespan({ 0, 0, 0, 0, 0, 0, 2 }, "2 years");
auto t = time_detailed{ 12, 1, 23, 3, 2, 5, 2 };
test_format_timespan(t, gen_as_str(t), "2 years");
t = time_detailed{ 0, 0, 0, 0, 0, 0, 0 };
BOOST_CHECK_EQUAL(utils::format_timespan(gen_as_time_t(t)), utils::format_timespan(0));
BOOST_CHECK_EQUAL(utils::format_timespan(gen_as_time_t(t), true), utils::format_timespan(0));
BOOST_CHECK_EQUAL(utils::format_timespan(gen_as_time_t(t)), utils::format_timespan(-10000));
BOOST_CHECK_EQUAL(utils::format_timespan(gen_as_time_t(t), true), utils::format_timespan(-10000));
test_format_timespan({ 4, 0, 49, 0, 0, 0, 0 }, "2 days, 1 hour, and 4 seconds", "2 days");
test_format_timespan({ 0, 40, 0, 11, 1, 0, 4 }, "4 years, 2 weeks, 4 days, and 40 minutes", "4 years");
test_format_timespan({ 0, 0, 1, 0, 0, 3, 4 }, "4 years, 3 months, and 1 hour", "4 years");
test_format_timespan({ 10, 0, 0, 0, 0, 2, 0 }, "2 months and 10 seconds", "2 months");
{
constexpr auto time = chrono::months{2} + std::chrono::seconds{10};
BOOST_CHECK_EQUAL("2 months and 10 seconds", utils::format_timespan(time, true));
BOOST_CHECK_EQUAL("2 months", utils::format_timespan(time));
}
}
BOOST_AUTO_TEST_SUITE_END()