Format a Timespan with Years

Format A TimeSpan With Years

A TimeSpan doesn't have a sensible concept of "years" because it depends on the start and end point. (Months is similar - how many months are there in 29 days? Well, it depends...)

To give a shameless plug, my Noda Time project makes this really simple though:

using System;
using NodaTime;

public class Test
{
static void Main(string[] args)
{
LocalDate start = new LocalDate(2010, 6, 19);
LocalDate end = new LocalDate(2013, 4, 11);
Period period = Period.Between(start, end,
PeriodUnits.Years | PeriodUnits.Days);

Console.WriteLine("Between {0} and {1} are {2} years and {3} days",
start, end, period.Years, period.Days);
}
}

Output:

Between 19 June 2010 and 11 April 2013 are 2 years and 296 days

A Real Timespan Object With .Years & .Months

Here is the main answer with code, please note that you can get any number of dates/times accuracy, seconds & minutes, or seconds, minutes and days, anywhere up to years (which would contain 6 parts/segments). If you specify top two and it's over a year old, it will return "1 year and 3 months ago" and won't return the rest because you've requested two segments. if it's only a few hours old, then it will only return "2 hours and 1 minute ago". Of course, same rules apply if you specify 1, 2, 3, 4, 5 or 6 segmets (maxes out at 6 because seconds, minutes, hours, days, months, years only make 6 types). It will also correct grammer issues like "minutes" vs "minute" depending on if it's 1 minute or more, same for all types, and the "string" generated will always be grammatically correct.

Here are some examples for use:
bAllowSegments identifies how many segments to show... ie: if 3, then return string would be (as an example)... "3 years, 2 months and 13 days" (won't include hours, minutes and seconds as the top 3 time categories are returned), if however, the date was a newer date, such as something a few days ago, specifying the same segments (3) will return "4 days, 1 hour and 13 minutes ago" instead, so it takes everything into account!

if bAllowSegments is 2 it would return "3 years and 2 months" and if 6 (maximum value) would return "3 years, 2 months, 13 days, 13 hours, 29 minutes and 9 seconds", but, be reminded that it will NEVER RETURN something like this "0 years, 0 months, 0 days, 3 hours, 2 minutes and 13 seconds ago" as it understands there is no date data in the top 3 segments and ignores them, even if you specify 6 segments, so don't worry :). Of course, if there is a segment with 0 in it, it will take that into account when forming the string, and will display as "3 days and 4 seconds ago" and ignoring the "0 hours" part! Enjoy and please comment if you like.

 Public Function RealTimeUntilNow(ByVal dt As DateTime, Optional ByVal bAllowSegments As Byte = 2) As String
' bAllowSegments identifies how many segments to show... ie: if 3, then return string would be (as an example)...
' "3 years, 2 months and 13 days" the top 3 time categories are returned, if bAllowSegments is 2 it would return
' "3 years and 2 months" and if 6 (maximum value) would return "3 years, 2 months, 13 days, 13 hours, 29 minutes and 9 seconds"
Dim rYears, rMonths, rDays, rHours, rMinutes, rSeconds As Int16
Dim dtNow = DateTime.Now
Dim daysInBaseMonth = Date.DaysInMonth(dt.Year, dt.Month)

rYears = dtNow.Year - dt.Year
rMonths = dtNow.Month - dt.Month
If rMonths < 0 Then rMonths += 12 : rYears -= 1 ' add 1 year to months, and remove 1 year from years.
rDays = dtNow.Day - dt.Day
If rDays < 0 Then rDays += daysInBaseMonth : rMonths -= 1
rHours = dtNow.Hour - dt.Hour
If rHours < 0 Then rHours += 24 : rDays -= 1
rMinutes = dtNow.Minute - dt.Minute
If rMinutes < 0 Then rMinutes += 60 : rHours -= 1
rSeconds = dtNow.Second - dt.Second
If rSeconds < 0 Then rSeconds += 60 : rMinutes -= 1

' this is the display functionality
Dim sb As StringBuilder = New StringBuilder()
Dim iSegmentsAdded As Int16 = 0

If rYears > 0 Then sb.Append(rYears) : sb.Append(" year" & If(rYears <> 1, "s", "") & ", ") : iSegmentsAdded += 1
If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

If rMonths > 0 Then sb.AppendFormat(rMonths) : sb.Append(" month" & If(rMonths <> 1, "s", "") & ", ") : iSegmentsAdded += 1
If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

If rDays > 0 Then sb.Append(rDays) : sb.Append(" day" & If(rDays <> 1, "s", "") & ", ") : iSegmentsAdded += 1
If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

If rHours > 0 Then sb.Append(rHours) : sb.Append(" hour" & If(rHours <> 1, "s", "") & ", ") : iSegmentsAdded += 1
If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

If rMinutes > 0 Then sb.Append(rMinutes) : sb.Append(" minute" & If(rMinutes <> 1, "s", "") & ", ") : iSegmentsAdded += 1
If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

If rSeconds > 0 Then sb.Append(rSeconds) : sb.Append(" second" & If(rSeconds <> 1, "s", "") & "") : iSegmentsAdded += 1

parseAndReturn:

' if the string is entirely empty, that means it was just posted so its less than a second ago, and an empty string getting passed will cause an error
' so we construct our own meaningful string which will still fit into the "Posted * ago " syntax...

If sb.ToString = "" Then sb.Append("less than 1 second")

Return ReplaceLast(sb.ToString.TrimEnd(" ", ",").ToString, ",", " and")

End Function

Of course, you will need a "ReplaceLast" function, which takes a source string, and an argument specifying what needs to be replaced, and another arg specifying what you want to replace it with, and it only replaces the last occurance of that string... i've included my one if you don't have one or dont want to implement it, so here it is, it will work "as is" with no modification needed. I know the reverseit function is no longer needed (exists in .net) but the ReplaceLast and the ReverseIt func are carried over from the pre-.net days, so please excuse how dated it may look (still works 100% tho, been using em for over ten years, can guarante they are bug free)... :). Also, if you are using VB6, you can use StrReverse (wrapping it around the string extended with the .ReverseIt extension method), instead of using the ReverseIt() function (provided as an extension method). So, instead of doing sReplacable.ReverseIt, you'd do StrReverse(sReplacable) as StrReverse() is a built in VB6 function (and does the exact same thing, reverses a given string, and does nothing more). If you use StrReverse() instead of my generic ReverseIt function, feel free to delete the ReverseIt function/extension. StrReverse() function should be available in .NET as long as you are importing the legacy ms-visualbasic-dll library. Makes no difference either way, I had written ReverseIt() before I even know a StrReverse() function had existed, and had been using it ever since out of habit (no real reason to use mine as opposed to the in-built generic function StrReverse) - in fact, I'm sure StrReverse (or a similar, newer .NET specific version of a string reversing function) would be written to be more efficient :). cheers.

<Extension()> _ 
Public Function ReplaceLast(ByVal sReplacable As String, ByVal sReplaceWhat As String, ByVal sReplaceWith As String) As String
' let empty string arguments run, incase we dont know if we are sending and empty string or not.
sReplacable = sReplacable.ReverseIt
sReplacable = Replace(sReplacable, sReplaceWhat.ReverseIt, sReplaceWith.ReverseIt, , 1) ' only does first item on reversed version!
Return sReplacable.ReverseIt.ToString
End Function

<Extension()> _
Public Function ReverseIt(ByVal strS As String, Optional ByVal n As Integer = -1) As String
Dim strTempX As String = "", intI As Integer

If n > strS.Length Or n = -1 Then n = strS.Length

For intI = n To 1 Step -1
strTempX = strTempX + Mid(strS, intI, 1)
Next intI

ReverseIt = strTempX + Right(strS, Len(strS) - n)

End Function

C# + Format TimeSpan

Try this:

Console.WriteLine("{0:D2}:{1:D2}", duration.Minutes, duration.Seconds);

C# Formatting TimeSpan to show a date

Using nodatime

var ld1 = new LocalDate(1991, 9, 20);
var ld2 = new LocalDate(2017, 1, 22);
var period = Period.Between(ld1, ld2);

Debug.WriteLine(period.Years);
Debug.WriteLine(period.Months);
Debug.WriteLine(period.Days);

Why does TimeSpan not have a Years property?

Programmer's life is really hard.

The length of year is variable. Some years have 365 days and some have 366 days. According to the calendar, some years could even have missing days. If talking about culture it becomes more difficult since Chinese lunar calendar can have 13 months a year.

The length of month is variable, and this is well-known. This is also to know that in other calendars things can get worse.

The length of day is variable, because of daylight savings and this is not just culture dependent but also geography dependent.

The length of hour and minute are variable, because of leap seconds.

It seems the only thing that is reliable is the length of a second. So internally, timespan is stored in seconds (or milliseconds, which is the same).

But the variability of time units makes the answer "how many (years/months/days/hours/minites) for n seconds?" being always inaccurate.

This is why the developers end up with a solution that is useful in practical but not precise. They simply ignore daylight savings and leap seconds. However, since people hardly ask about years and months, they just decided not to answer those questions.

How do I convert a TimeSpan to a formatted string?

Would TimeSpan.ToString() do the trick for you? If not, it looks like the code sample on that page describes how to do custom formatting of a TimeSpan object.

How to format a TimeSpan for hours not days

According to MSDN, using %h will show you

The number of whole hours in the time interval that are not counted as part of days.

I think you will need to use the TotalHours property of the TimeSpan class like:

TimeSpan day= new TimeSpan(TimeSpan.TicksPerDay);
Console.WriteLine("{0} hours {1} minutes", (int)day.TotalHours, day.Minutes);

Update

If you absolutely need to be able to achieve the stated format by passing custom formatters to the ToString method, you will probably need to create your own CustomTimeSpan class. Unfortunately, you cannot inherit from a struct, so you will have to build it from the ground up.

Timespan formatting

There is no built-in functionality for this, you'll need to use a custom method, something like:

TimeSpan ts = new TimeSpan(0, 70, 0);
String.Format("{0} hour{1} {2} minute{3}",
ts.Hours,
ts.Hours == 1 ? "" : "s",
ts.Minutes,
ts.Minutes == 1 ? "" : "s")

How to format DateTime / TimeSpan from String in C#

The code is okay for the requirements you have listed, you could consider hiding some of the logic away by creating an extension method and using DateTime.Hour in your if statement:

public static class StringExtensions
{
public static IEnumerable<DateTime> ToDateTimePairs(this string input)
{
var dates = input.Split('-').Select(x => DateTime.Parse(x.Trim(), CultureInfo.GetCultureInfo("en-NZ"))).ToList();

if (dates[1].Hour < dates[0].Hour)
{
dates[1] = dates[1].AddDays(1);
}

return dates;
}
}

Your code then becomes:

string input = "7:00 am - 12:00 am";
var dates = input.ToDateTimePairs();
foreach(var date in dates)
{
Console.WriteLine(date);
}
Console.ReadKey();

Aside: You could also add some validation steps in ToDateTimePairs() (you should only have two valid DateTime objects after splitting the string, etc). The way you implement it is up to you; right now, if an invalid date is contained in the string, the call to DateTime.Parse() will throw a FormatException - if you'd like to handle the validation of the parsing yourself, consider using DateTime.TryParse() instead.



Related Topics



Leave a reply



Submit