Introduction
In this part of the series an unorthodox technique will be used to create a class project which uses an NuGet package NodaTime for working with time zones.
The technique, in this case is sloppy at the start and is polished once all pieces have been ironed out.
Part 1 of the series
🛑 Sloppy is not for everyone but when done as shown below can save time especially when carving new ground with unknown techniques, new libraries etc.
Tip 1
At the time of this article, many developers do not care for Windows Forms projects, but they are better than proofing out code than using console projects. With a Console project a developer when trying out code will need to place variations of code into separate methods and comment out code which is not relevant while with Window Forms create buttons and place code for variations into each button so no code commenting.
Once an acceptable solution is decided on, move the code to classes.
Tip 2
Write classes directly in the form rather than creating a new physical file. Everything is lumped together, yes sloppy but no need to tab between files and easy to modify and/or delete if desired.
Once an acceptable solution is decided on, move to separate files and even move to a class project which is better as the Window Forms project is strictly in most cases a place to iron out a design. Of course, if the code is for multiple platforms other modifications will be in order.
Tip 3
Take time to read documentation when working through task and search for technical blogs as resources. Also, make sure the information applies to the NET Framework that a project is targeting which can save time in the long run.
Tip 4
Once code is finalized, use GitHub Copilot to document the code. All provided code has been documented except in a few cases by Copilot.
NodaTime/Time zones
The task is to get time offsets and does a time zone support daylight savings time for PST, EST and the South of Mexico.
As mentioned, NodaTime will be used. To Get a specific time zone a list of time zones is required and is found here.
Next step is to use your skills for searching the web for what is needed from NodaTime which means the following is needed and do not get hung up on variable names, remember sloppy for now.
public DateTime GetSysDateTimeNow(string zone = "US/Pacific")
{
Instant now = SystemClock.Instance.GetCurrentInstant();
var shanghaiZone = DateTimeZoneProviders.Tzdb[zone];
return now.InZone(shanghaiZone).ToDateTimeUnspecified();
}
Still sloppy, create a button, double click and add the following code.
private void asiaButton_Click(object sender, EventArgs e)
{
//Instant now = SystemClock.Instance.GetCurrentInstant();
//ZonedDateTime nowInIsoUtc = now.InUtc();
//var pacificNorthWest = GetSysDateTimeNow();
var cancun = GetSysDateTimeNow("America/Cancun");
var offset = GetTimeZoneOffset("America/Cancun",
DateTime.SpecifyKind(cancun, DateTimeKind.Utc));
Debug.WriteLine(offset.Hours);
Debug.WriteLine(offset.ToString(@"hh\:mm"));
}
Run the project, well as it turns out the results are incorrect. The problem stems from not taking into account if the time zone supports daylight savings. After a few more attempts the following works, but why?
Because the developer figured out by using Visual Studio's debugger that DateTimeZone has two properties, MinOffset and MaxOffset of type Offset. Min for daylight savings.
In the form, another class is created, TimeOperations with TimeZoneOffset method.
Important The first iteration had IsDaylightSavingsTime in the method TimeZoneOffset then extracted for reuse as we want more than one time zone.
internal static class TimeOperations
{
/// <summary>
/// Gets the time zone offset and whether daylight savings is in effect for the specified time zone and UTC date and time.
/// </summary>
/// <param name="timeZoneId">The identifier of the time zone.</param>
/// <param name="dateTimeUtc">The UTC date and time.</param>
/// <returns>A tuple containing the time zone offset as a string and a boolean indicating whether daylight savings is in effect.</returns>
public static (string offset, bool daylightSavings) TimeZoneOffset(string timeZoneId, DateTime dateTimeUtc)
{
Instant instant = Instant.FromDateTimeUtc(dateTimeUtc);
DateTimeZone zone = DateTimeZoneProviders.Tzdb[timeZoneId];
ZonedDateTime dateTime = new(instant, zone);
var daylightSavings = dateTime.IsDaylightSavingsTime();
Offset offset = daylightSavings ? zone.MinOffset : zone.MaxOffset;
return (offset.ToString("m", null), daylightSavings);
}
}
/// <summary>
/// Determines whether the specified date and time is during daylight saving time in the given time zone.
/// </summary>
/// <param name="zonedDateTime">The date and time to check.</param>
/// <returns>True if the specified date and time is during daylight saving time; otherwise, false.</returns>
public static bool IsDaylightSavingsTime(this ZonedDateTime zonedDateTime)
{
var instant = zonedDateTime.ToInstant();
var zoneInterval = zonedDateTime.Zone.GetZoneInterval(instant);
return zoneInterval.Savings != Offset.Zero;
}
In another Button click event test the above code.
✳️ Optional: Once completely hashed out (not yet) consider packaging the class project as a NuGet package as a local or internet package.
Refining code/neat and tidy
Once the code fits the task at hand, move all code out of the form and into a class project so the code can be used in one or more projects. Currently the task is for one project but later may very well be need for other projects.
The desired finished product for, in this case, each country be uniform as shown below.
For the United States
A class for the east coast.
/// <summary>
/// Represents the East Coast time zone.
/// </summary>
public class EastCoast
{
private string _timeZoneId => "US/Eastern";
/// <summary>
/// Gets the offset of the East Coast time zone.
/// </summary>
/// <returns>The offset of the East Coast time zone.</returns>
public string Offset =>
TimeOperations.TimeZoneOffset(_timeZoneId,
DateTime.SpecifyKind(TimeOperations.GetSysDateTimeNow(_timeZoneId),
DateTimeKind.Utc)).offset;
/// <summary>
/// Determines whether the East Coast time zone is currently in daylight saving time.
/// </summary>
/// <returns>True if the East Coast time zone is currently in daylight saving time; otherwise, false.</returns>
public bool DaylightSavingsSupported =>
TimeOperations.TimeZoneOffset(_timeZoneId,
DateTime.SpecifyKind(TimeOperations.GetSysDateTimeNow(_timeZoneId),
DateTimeKind.Utc)).daylightSavings;
}
A class for the west coast.
public class WestCoast
{
private string _timeZoneId => "US/Pacific";
/// <summary>
/// Gets the offset of the West Coast time zone.
/// </summary>
/// <returns>The offset of the West Coast time zone.</returns>
public string Offset =>
TimeOperations.TimeZoneOffset(_timeZoneId,
DateTime.SpecifyKind(TimeOperations.GetSysDateTimeNow(),
DateTimeKind.Utc)).offset;
/// <summary>
/// Determines whether the West Coast time zone is currently in daylight saving time.
/// </summary>
/// <returns>True if the West Coast time zone is currently in daylight saving time; otherwise, false.</returns>
public bool DaylightSavingsSupported =>
TimeOperations.TimeZoneOffset(_timeZoneId,
DateTime.SpecifyKind(TimeOperations.GetSysDateTimeNow(_timeZoneId),
DateTimeKind.Utc)).daylightSavings;
}
Where both of the above classes are used in the following class.
/// <summary>
/// Represents the United States with its East Coast and West Coast time zones.
/// </summary>
public class UnitedStates
{
/// <summary>
/// Gets or sets the East Coast time zone.
/// </summary>
public EastCoast EastCoast { get; init; } = new();
/// <summary>
/// Gets or sets the West Coast time zone.
/// </summary>
public WestCoast WestCoast { get; init; } = new();
}
Then Mexico follows the same pattern as done for the United States.
public class SeaCoast
{
private string _timeZoneId => "America/Cancun";
/// <summary>
/// Gets the offset of the West Coast time zone.
/// </summary>
/// <returns>The offset of the West Coast time zone.</returns>
public string Offset =>
TimeOperations.TimeZoneOffset(_timeZoneId,
DateTime.SpecifyKind(TimeOperations.GetSysDateTimeNow(),
DateTimeKind.Utc)).offset;
/// <summary>
/// Determines whether the West Coast time zone is currently in daylight saving time.
/// </summary>
/// <returns>True if the West Coast time zone is currently in daylight saving time; otherwise, false.</returns>
public bool DaylightSavingsSupported =>
TimeOperations.TimeZoneOffset(_timeZoneId,
DateTime.SpecifyKind(TimeOperations.GetSysDateTimeNow(_timeZoneId),
DateTimeKind.Utc)).daylightSavings;
}
public class Mexico
{
public SeaCoast SeaCoast { get; init; } = new();
}
Both above code samples are starters as there are more time zones. They were intentionally left out for the reader to work on as simply read and in some cases copy-n-pasting code done without actually trying is fruitless in the learning process.
Neat - no sloppy
Many developers want to find out the age of a person. In the following, still using NodaTime the lesson is twofold, learn how to get the age of a person and learn a little more about NodaTime, in this case the Period Class.
The Period class represents a period of time expressed in human chronological terms: hours, days, weeks, months and so on.
Getting a person's age turns out to be a one liner.
using NodaTime;
namespace NodaTimeLibrary.Classes;
public class PeriodOperations
{
/// <summary>
/// Calculates the period between a birthdate and a given date in years, months, and days.
/// </summary>
/// <param name="birthDate">The birthdate.</param>
/// <param name="date">The date to calculate the period to.</param>
/// <returns>The period between the birthdate and the given date.</returns>
public static Period YearsOld(DateOnly birthDate, DateOnly date) =>
Period.Between(LocalDate.FromDateOnly(birthDate), LocalDate.FromDateOnly(date),
PeriodUnits.Years |
PeriodUnits.Months |
PeriodUnits.Days);
}
Trying out in a Console project using Spectre.Console NuGet package for date prompting.
namespace NodaTimeDemoApp.Classes;
internal class Prompts
{
public static DateOnly GetBirthDate() =>
AnsiConsole.Prompt(
new TextPrompt<DateOnly>("What is your [white]birth date[/]?")
.PromptStyle("yellow")
.DefaultValue(new DateOnly(1956,9,24))
.ValidationErrorMessage(
"[red]Please enter a valid date or press ENTER to not enter a date[/]")
.Validate(dateTime => dateTime.Year switch
{
>= 1920 => ValidationResult.Error("[red]Must be less than 1920[/]"),
_ => ValidationResult.Success(),
})
.AllowEmpty());
}
In the frontend.
private static void GetYearsOld()
{
PrintCyan();
DateOnly birthDate = Prompts.GetBirthDate();
DateOnly today = DateOnly.FromDateTime(DateTime.Now);
var period = PeriodOperations.YearsOld(birthDate, today);
AnsiConsole.MarkupLine($"[b]{period.Years}[/] years " +
$"[b]{period.Months}[/] months " +
$"[b]{period.Days}[/] days");
}
Source code
NodaTime Resources
- Documentation
- Recipes This page collects a lot of them in one place. A lot of these are small, because once you know what operation you're trying to achieve, Noda Time makes it very easy to express that... but it can still be tricky finding that if you're new to the API.
Summary
When writing code at any level it is okay to start out with sloppy code with the intent to clean up and refactor. This works best when learning something new although this can also work with techniques that are known.
The author uses sloppy code in tangent with refactoring features in Jetbrains ReSharper.