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:
parent
70a76b0e21
commit
2c216eec72
8 changed files with 185 additions and 191 deletions
115
src/formula/format_timespan.hpp
Normal file
115
src/formula/format_timespan.hpp
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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"];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Reference in a new issue