TimeSpan 到友好字符串库 (C#)

发布于 2024-07-28 18:02:04 字数 217 浏览 8 评论 0原文

有谁知道一个好的库(或代码片段)用于将 TimeSpan 对象转换为“友好”字符串,例如:

  • 两年三个月零四天
  • 一周零两天

(它用于文档过期系统,其中过期时间可以是几天到几十年的任何时间)

只是为了澄清一下,假设我有一个 7 天的 TimeSpan,应该打印“1 周”、14 天“2 周”、366 天“1 年零 1 天”等ETC。

Does anyone know of a good library (or code snippet) for converting a TimeSpan object to a "friendly" string such as:

  • Two years, three months and four days
  • One week and two days

(It's for a document expiry system, where the expiry could be anything from a few days to several decades)

Just to clarify, say I had a TimeSpan with 7 days, that should print "1 week", 14 days "2 weeks", 366 days "1 year and 1 day", etc etc.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

任谁 2024-08-04 18:03:05

请参阅how-do-i-calculate-relative-time,询问(< a href="https://stackoverflow.com/users/1/jeff-atwood">由 1 号用户)一年前,当时 SO 还很年轻,还没有公开。
它(可能)是 SO 问题和答案当前年龄显示的基础。

See how-do-i-calculate-relative-time, asked (by user number 1) a year ago when SO was young and not public.
It was (probably) the basis for the current age display for SO questions and answers.

童话 2024-08-04 18:02:59

是的! 我多次需要同样的东西,现在我创建了我的 onw 包并将其发布在 Nuget 中。 欢迎您使用它。
包名为EstecheAssemblies

实现起来很简单:

using EstecheAssemblies;  

var date = new DateTime("2019-08-08 01:03:21");  
var text = date.ToFriendlyTimeSpan();

Yes! I needed the same so many times then now I create my onw package and published it in Nuget. You are welcome to use it.
The package name is EstecheAssemblies

It's easy to implement:

using EstecheAssemblies;  

var date = new DateTime("2019-08-08 01:03:21");  
var text = date.ToFriendlyTimeSpan();
陪你到最终 2024-08-04 18:02:53

要格式化任何超过 1 天的时间段(即月/年/十年等),Timespan 对象是不够的。

假设您的时间跨度为 35 天,那么从 4 月 1 日起,您将获得 1 个月零 5 天,而从 12 月 1 日起,您将获得 1 个月零 4 天。

To format any period longer than 1 day (i.e. month/year/decade etc.) a Timespan object is not enough.

Suppose your timespan is 35 days, then from Apr 1 you would get one month and five days, whereas from Dec 1 you would get one month and four days.

如果没有 2024-08-04 18:02:48

它可能不会完成您想要的所有操作,但在 v4 中,Microsoft 将 TimeSpan 上实现 IFormattable

It probably won't do everything you are looking for, but in v4 Microsoft will be implementing IFormattable on TimeSpan.

短暂陪伴 2024-08-04 18:02:43

这是我的解决方案。 它基于此线程中的其他答案,并添加了对年份和月份的支持,正如原始问题中所要求的(也是我所需要的)。

至于讨论这是否有意义,我想说的是,确实有这样的情况。 就我而言,我们想要展示协议的持续时间,在某些情况下只有几天,而在其他情况下则长达几年。

测试;

[Test]
public void ToFriendlyDuration_produces_expected_result()
{
    new DateTime(2019, 5, 28).ToFriendlyDuration(null).Should().Be("Until further notice");
    new DateTime(2019, 5, 28).ToFriendlyDuration(new DateTime(2020, 5, 28)).Should().Be("1 year");
    new DateTime(2019, 5, 28).ToFriendlyDuration(new DateTime(2021, 5, 28)).Should().Be("2 years");
    new DateTime(2019, 5, 28).ToFriendlyDuration(new DateTime(2021, 8, 28)).Should().Be("2 years, 3 months");
    new DateTime(2019, 5, 28).ToFriendlyDuration(new DateTime(2019, 8, 28)).Should().Be("3 months");
    new DateTime(2019, 5, 28).ToFriendlyDuration(new DateTime(2019, 8, 31)).Should().Be("3 months, 3 days");
    new DateTime(2019, 5, 1).ToFriendlyDuration(new DateTime(2019, 5, 31)).Should().Be("30 days");
    new DateTime(2010, 5, 28).ToFriendlyDuration(new DateTime(2020, 8, 28)).Should().Be("10 years, 3 months");
    new DateTime(2010, 5, 28).ToFriendlyDuration(new DateTime(2020, 5, 29)).Should().Be("10 years, 1 day");
}

执行;

private class TermAndValue
{
    public TermAndValue(string singular, string plural, int value)
    {
        Singular = singular;
        Plural = plural;
        Value = value;
    }

    public string Singular { get; }
    public string Plural { get; }
    public int Value { get; }
    public string Term => Value > 1 ? Plural : Singular;
}

public static string ToFriendlyDuration(this DateTime value, DateTime? endDate, int maxNrOfElements = 2)
{
    if (!endDate.HasValue)
        return "Until further notice";

    var extendedTimeSpan = new TimeSpanWithYearAndMonth(value, endDate.Value);
    maxNrOfElements = Math.Max(Math.Min(maxNrOfElements, 5), 1);
    var termsAndValues = new[]
    {
        new TermAndValue("year", "years", extendedTimeSpan.Years),
        new TermAndValue("month", "months", extendedTimeSpan.Months),
        new TermAndValue("day", "days", extendedTimeSpan.Days),
        new TermAndValue("hour", "hours", extendedTimeSpan.Hours),
        new TermAndValue("minute", "minutes", extendedTimeSpan.Minutes)
    };

    var parts = termsAndValues.Where(i => i.Value != 0).Take(maxNrOfElements);

    return string.Join(", ", parts.Select(p => $"{p.Value} {p.Term}"));
}

internal class TimeSpanWithYearAndMonth
{
    internal TimeSpanWithYearAndMonth(DateTime startDate, DateTime endDate)
    {
        var span = endDate - startDate;

        Months = 12 * (endDate.Year - startDate.Year) + (endDate.Month - startDate.Month);
        Years = Months / 12;
        Months -= Years * 12;

        if (Months == 0 && Years == 0)
        {
            Days = span.Days;
        }
        else
        {
            var startDateExceptYearsAndMonths = startDate.AddYears(Years);
            startDateExceptYearsAndMonths = startDateExceptYearsAndMonths.AddMonths(Months);
            Days = (endDate - startDateExceptYearsAndMonths).Days;
        }

        Hours = span.Hours;
        Minutes = span.Minutes;
    }

    public int Minutes { get; }
    public int Hours { get; }
    public int Days { get; }
    public int Years { get; }
    public int Months { get; }
}

Here is my solution to this. It is based on other answers in this thread, with added support for year and month as that was requested in the original question (and was what I needed).

As for the discussion whether or not this makes sense I would say that there are cases where it does so. In my case we wanted to show the duration of agreements that in some cases are just a few days, and in other cases several years.

Tests;

[Test]
public void ToFriendlyDuration_produces_expected_result()
{
    new DateTime(2019, 5, 28).ToFriendlyDuration(null).Should().Be("Until further notice");
    new DateTime(2019, 5, 28).ToFriendlyDuration(new DateTime(2020, 5, 28)).Should().Be("1 year");
    new DateTime(2019, 5, 28).ToFriendlyDuration(new DateTime(2021, 5, 28)).Should().Be("2 years");
    new DateTime(2019, 5, 28).ToFriendlyDuration(new DateTime(2021, 8, 28)).Should().Be("2 years, 3 months");
    new DateTime(2019, 5, 28).ToFriendlyDuration(new DateTime(2019, 8, 28)).Should().Be("3 months");
    new DateTime(2019, 5, 28).ToFriendlyDuration(new DateTime(2019, 8, 31)).Should().Be("3 months, 3 days");
    new DateTime(2019, 5, 1).ToFriendlyDuration(new DateTime(2019, 5, 31)).Should().Be("30 days");
    new DateTime(2010, 5, 28).ToFriendlyDuration(new DateTime(2020, 8, 28)).Should().Be("10 years, 3 months");
    new DateTime(2010, 5, 28).ToFriendlyDuration(new DateTime(2020, 5, 29)).Should().Be("10 years, 1 day");
}

Implementation;

private class TermAndValue
{
    public TermAndValue(string singular, string plural, int value)
    {
        Singular = singular;
        Plural = plural;
        Value = value;
    }

    public string Singular { get; }
    public string Plural { get; }
    public int Value { get; }
    public string Term => Value > 1 ? Plural : Singular;
}

public static string ToFriendlyDuration(this DateTime value, DateTime? endDate, int maxNrOfElements = 2)
{
    if (!endDate.HasValue)
        return "Until further notice";

    var extendedTimeSpan = new TimeSpanWithYearAndMonth(value, endDate.Value);
    maxNrOfElements = Math.Max(Math.Min(maxNrOfElements, 5), 1);
    var termsAndValues = new[]
    {
        new TermAndValue("year", "years", extendedTimeSpan.Years),
        new TermAndValue("month", "months", extendedTimeSpan.Months),
        new TermAndValue("day", "days", extendedTimeSpan.Days),
        new TermAndValue("hour", "hours", extendedTimeSpan.Hours),
        new TermAndValue("minute", "minutes", extendedTimeSpan.Minutes)
    };

    var parts = termsAndValues.Where(i => i.Value != 0).Take(maxNrOfElements);

    return string.Join(", ", parts.Select(p => $"{p.Value} {p.Term}"));
}

internal class TimeSpanWithYearAndMonth
{
    internal TimeSpanWithYearAndMonth(DateTime startDate, DateTime endDate)
    {
        var span = endDate - startDate;

        Months = 12 * (endDate.Year - startDate.Year) + (endDate.Month - startDate.Month);
        Years = Months / 12;
        Months -= Years * 12;

        if (Months == 0 && Years == 0)
        {
            Days = span.Days;
        }
        else
        {
            var startDateExceptYearsAndMonths = startDate.AddYears(Years);
            startDateExceptYearsAndMonths = startDateExceptYearsAndMonths.AddMonths(Months);
            Days = (endDate - startDateExceptYearsAndMonths).Days;
        }

        Hours = span.Hours;
        Minutes = span.Minutes;
    }

    public int Minutes { get; }
    public int Hours { get; }
    public int Days { get; }
    public int Years { get; }
    public int Months { get; }
}
玻璃人 2024-08-04 18:02:38

现在还有 Humanizer 项目,看起来非常有趣,可以做到这一点以及更多。

There is now also the Humanizer project that looks very interesting that can do this and way more.

你在我安 2024-08-04 18:02:32

TimeSpan 对象具有 Days、Hours、Minutes 和 Seconds 属性,因此制作将这些值格式化为友好字符串的代码片段不会太难。

不幸的是,天数是最大的值。 如果超过这个时间,您将不得不开始担心每年一个月中的几天......等等。 在我看来,你最好停下来几天(额外的努力似乎不值得)。

更新

...我想我应该从我自己的评论中提出这一点:

可以理解,但是“此文档将在 10 年 3 个月 21 天 2 小时 30 分钟后过期”真的更有帮助还是不那么愚蠢? 如果由我决定,因为这两种表示似乎都没有什么用...

我会保留到期时间,直到日期相当接近(如果您担心得到,可能是 30 或 60 天)文档已更新)。

对我来说似乎是更好的用户体验选择。

The TimeSpan object has Days, Hours, Minutes, and Seconds properties on it, so it wouldn't be too hard to make a snippet that formats those values to a friendly string.

Unfortunately Days is the largest value. Anything longer than that and you'll have to start worrying about days in a month for every year...etc. You're better off stopping at days in my opinion (the added effort doesn't seem worth the gain).

UPDATE

...I figured I'd bring this up from my own comment:

Understandable, but is "This document expires in 10 years, 3 months, 21 days, 2 hours, and 30 minutes" really any more helpful or less silly? If it were up to me, since neither representation seems very useful...

I'd leave off the timespan for expiry until the date got reasonably close (30 or 60 days maybe if you're worried about getting the document updated).

Seems a much better UX choice to me.

看海 2024-08-04 18:02:27

我知道这已经很旧了,但我想用一个很棒的 nuget 包来回答。

Install-Package Humanizer

https://www.nuget.org/packages/Humanizer

https://github.com/MehdiK/Humanizer

他们的 readme.md 中的示例

TimeSpan.FromMilliseconds(1299630020).Humanize(4) => "2 weeks, 1 day, 1 hour, 30 seconds"

@ian-becker 需要信用

I know this is old, but I wanted to answer with a great nuget package.

Install-Package Humanizer

https://www.nuget.org/packages/Humanizer

https://github.com/MehdiK/Humanizer

Example from their readme.md

TimeSpan.FromMilliseconds(1299630020).Humanize(4) => "2 weeks, 1 day, 1 hour, 30 seconds"

@ian-becker Needs the credit

简单 2024-08-04 18:02:21

不是一个功能齐全的实现,但它应该让您足够接近。

DateTime dtNow = DateTime.Now;
DateTime dtYesterday = DateTime.Now.AddDays(-435.0);
TimeSpan ts = dtNow.Subtract(dtYesterday);

int years = ts.Days / 365; //no leap year accounting
int months = (ts.Days % 365) / 30; //naive guess at month size
int weeks = ((ts.Days % 365) % 30) / 7;
int days = (((ts.Days % 365) % 30) % 7);

StringBuilder sb = new StringBuilder();
if(years > 0)
{
    sb.Append(years.ToString() + " years, ");
}
if(months > 0)
{
    sb.Append(months.ToString() + " months, ");
}
if(weeks > 0)
{
    sb.Append(weeks.ToString() + " weeks, ");
}
if(days > 0)
{
    sb.Append(days.ToString() + " days.");
}
string FormattedTimeSpan = sb.ToString();

最后,您真的需要让别人知道文档将在 1 年、5 个月、2 周和 3 天后到期吗? 您不能告诉他们该文件将在 1 年后或 5 个月后过期吗? 只需取最大的单位并说出该单位的 n 即可。

Not a fully featured implementation, but it should get you close enough.

DateTime dtNow = DateTime.Now;
DateTime dtYesterday = DateTime.Now.AddDays(-435.0);
TimeSpan ts = dtNow.Subtract(dtYesterday);

int years = ts.Days / 365; //no leap year accounting
int months = (ts.Days % 365) / 30; //naive guess at month size
int weeks = ((ts.Days % 365) % 30) / 7;
int days = (((ts.Days % 365) % 30) % 7);

StringBuilder sb = new StringBuilder();
if(years > 0)
{
    sb.Append(years.ToString() + " years, ");
}
if(months > 0)
{
    sb.Append(months.ToString() + " months, ");
}
if(weeks > 0)
{
    sb.Append(weeks.ToString() + " weeks, ");
}
if(days > 0)
{
    sb.Append(days.ToString() + " days.");
}
string FormattedTimeSpan = sb.ToString();

In the end, do you really need to let someone know a document is going to expire exactly 1 year, 5 months, 2 weeks, and 3 days from now? Can't you get by with telling them the document will expire over 1 year from now, or over 5 months from now? Just take the largest unit and say over n of that unit.

红玫瑰 2024-08-04 18:02:16

我只是偶然发现这个问题,因为我想做类似的事情。 经过一番谷歌搜索后,我仍然没有找到我想要的东西:以某种“圆形”方式显示时间跨度。 我的意思是:当某些事件持续了几天时,显示毫秒并不总是有意义的。 然而,当需要几分钟时,它可能会发生。 在这种情况下,我不想显示 0 天和 0 小时。 因此,我想参数化要显示的相关时间跨度部分的数量。 这导致了这段代码:

public static class TimeSpanExtensions
{
    private enum TimeSpanElement
    {
        Millisecond,
        Second,
        Minute,
        Hour,
        Day
    }

    public static string ToFriendlyDisplay(this TimeSpan timeSpan, int maxNrOfElements)
    {
        maxNrOfElements = Math.Max(Math.Min(maxNrOfElements, 5), 1);
        var parts = new[]
                        {
                            Tuple.Create(TimeSpanElement.Day, timeSpan.Days),
                            Tuple.Create(TimeSpanElement.Hour, timeSpan.Hours),
                            Tuple.Create(TimeSpanElement.Minute, timeSpan.Minutes),
                            Tuple.Create(TimeSpanElement.Second, timeSpan.Seconds),
                            Tuple.Create(TimeSpanElement.Millisecond, timeSpan.Milliseconds)
                        }
                                    .SkipWhile(i => i.Item2 <= 0)
                                    .Take(maxNrOfElements);

        return string.Join(", ", parts.Select(p => string.Format("{0} {1}{2}", p.Item2, p.Item1, p.Item2 > 1 ? "s" : string.Empty)));
    }
}

示例 (LinqPad):

new TimeSpan(1,2,3,4,5).ToFriendlyDisplay(3).Dump();
new TimeSpan(0,5,3,4,5).ToFriendlyDisplay(3).Dump();

显示:

1 Day, 2 Hours, 3 Minutes
5 Hours, 3 Minutes, 4 Seconds

适合我,看看它是否适合您。

I just stumbled upon this question because I wanted to do a similar thing. After some googling I still didn't find what I wanted: display a timespan in a sort of "rounded" fashion. I mean: when some event took several days, it doesn't always make sense to display the milliseconds. However, when it took minutes, it probably does. And in that case, I don't want 0 days and 0 hours to be displayed. So, I want to parametrize the number of relevant timespan parts to be displayed. This resulted in this bit of code:

public static class TimeSpanExtensions
{
    private enum TimeSpanElement
    {
        Millisecond,
        Second,
        Minute,
        Hour,
        Day
    }

    public static string ToFriendlyDisplay(this TimeSpan timeSpan, int maxNrOfElements)
    {
        maxNrOfElements = Math.Max(Math.Min(maxNrOfElements, 5), 1);
        var parts = new[]
                        {
                            Tuple.Create(TimeSpanElement.Day, timeSpan.Days),
                            Tuple.Create(TimeSpanElement.Hour, timeSpan.Hours),
                            Tuple.Create(TimeSpanElement.Minute, timeSpan.Minutes),
                            Tuple.Create(TimeSpanElement.Second, timeSpan.Seconds),
                            Tuple.Create(TimeSpanElement.Millisecond, timeSpan.Milliseconds)
                        }
                                    .SkipWhile(i => i.Item2 <= 0)
                                    .Take(maxNrOfElements);

        return string.Join(", ", parts.Select(p => string.Format("{0} {1}{2}", p.Item2, p.Item1, p.Item2 > 1 ? "s" : string.Empty)));
    }
}

Example (LinqPad):

new TimeSpan(1,2,3,4,5).ToFriendlyDisplay(3).Dump();
new TimeSpan(0,5,3,4,5).ToFriendlyDisplay(3).Dump();

Displays:

1 Day, 2 Hours, 3 Minutes
5 Hours, 3 Minutes, 4 Seconds

Suits me, see if it suits you.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文