diff --git a/src/Avalonia.Controls/DateTimePickers/DateTimeFormatter.cs b/src/Avalonia.Controls/DateTimePickers/DateTimeFormatter.cs new file mode 100644 index 0000000000..46b1c07ebc --- /dev/null +++ b/src/Avalonia.Controls/DateTimePickers/DateTimeFormatter.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Avalonia.Controls +{ + /// + /// Formats a DateTimeOffset by the specified pattern + /// Based on, but not replica, the Windows.Globalization.DateTimeFormatting.DateTimeFormatter + /// https://docs.microsoft.com/en-us/uwp/api/windows.globalization.datetimeformatting.datetimeformatter?view=winrt-19041 + /// + /// Currently only the Gregorian Calendar is supported, and regions and languages are not supported. + /// Formatting timezones is also not supported, that's a large task + /// + /// + /// /// Most formats used in UWP/WinUI are compatible here, but not all + /// This DateTimeFormatter will also work with TimeSpans (only patterns though), + /// in addition to the default DateTime/DateTimeOffset + /// + /// + /// Formats are broken down into Patterns and Templates, which are "complete" patterns + /// If a Template is used, only 1 may be provided and cannot be mixed with anything else + /// Multiple patterns can be specified, and can be mixed with other text. All patterns + /// must be enclosed in curly braces {}, e.g. {dayofweek}, or in xaml "{}{dayofweek}" + /// NOTE: Formats and Templates are case sensitive + /// + /// + public sealed class DateTimeFormatter + { + public DateTimeFormatter(string formatString) + { + _format = formatString; + var reg = new Regex("{([^{}]*)}"); + var results = reg.Matches(formatString).Cast().Select(m => m.Groups[1].Value).Distinct().ToList(); + Formats = results; + } + + public string Clock + { + get + { + if (_clock == null) + { + var timePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern; + if (timePattern.IndexOf("H") != -1) + return "24HourClock"; + return "12HourClock"; + } + return _clock; + } + set + { + _clock = value; + } + } + public List Formats { get; } + + /// + /// Formats a DateTimeOffset object by the format of the + /// + public string Format(DateTimeOffset toFormat) + { + string ret = _format; + + if (Formats.Count == 0) + { + return GetFormatTemplate(toFormat); + } + + foreach (var item in Formats) + { + var p = $"{{{item}}}"; + var ns = Regex.Replace(ret, $"{{{Regex.Escape(item)}}}", GetFormatValue(item, toFormat)); + ret = ns; + } + + return ret; + } + + /// + /// Formats a TimeSpan object by the format of the + /// + public string Format(TimeSpan toFormat) + { + string ret = _format; + + foreach (var item in Formats) + { + var p = $"{{{item}}}"; + var ns = Regex.Replace(ret, $"{{{Regex.Escape(item)}}}", GetFormatValue(item, toFormat)); + ret = ns; + } + + return ret; + } + + private string GetFormatValue(string pattern, DateTimeOffset dt) + { + var sp = pattern.Split(new[] { "." }, StringSplitOptions.None); + var type = sp[0].Trim(); + var desc = sp[1].Trim(); + var len = desc.Contains("(") ? int.Parse(desc.Substring(desc.IndexOf("(") + 1, desc.Length - desc.IndexOf(")"))) : -1; + if (type.Equals("era")) + { + return ""; + } + else if (type.Equals("year")) + { + var yr = dt.Year; + if (len == -1) + return yr.ToString(); + else if (len <= 2) + return yr.ToString().Substring(2); + else + return yr.ToString(); + } + else if (type.Equals("month")) + { + var mon = dt.Month; + var fmt = CultureInfo.CurrentCulture.DateTimeFormat; + if (len == -1) + return desc == "full" ? fmt.GetMonthName(dt.Month) : fmt.GetAbbreviatedMonthName(dt.Month); + var nm = desc == "full" ? fmt.GetMonthName(dt.Month) : fmt.GetAbbreviatedMonthName(dt.Month); + len = Math.Min(nm.Length, Math.Max(0, len)); + return nm.Substring(0, len); + } + else if (type.Equals("dayofweek")) + { + var dow = dt.DayOfWeek; + var fmt = CultureInfo.CurrentCulture.DateTimeFormat; + if (len == -1) + return desc == "full" ? fmt.GetDayName(dt.DayOfWeek) : fmt.GetAbbreviatedDayName(dt.DayOfWeek); + var nm = desc == "full" ? fmt.GetDayName(dt.DayOfWeek) : fmt.GetAbbreviatedDayName(dt.DayOfWeek); + len = Math.Min(nm.Length, Math.Max(0, len)); + return nm.Substring(0, len); + } + else if (type.Equals("day")) + { + var dy = dt.Day; + if (len == -1) + return dy.ToString(); + if (len < 1 || len > 2) + len = 2; + return dy.ToString($"D{len}"); + } + else if (type.Equals("period")) + { + if (Clock == "24HourClock") + return ""; + + var hr = dt.Hour; + if (hr >= 12) + return CultureInfo.CurrentCulture.DateTimeFormat.PMDesignator; + else + return CultureInfo.CurrentCulture.DateTimeFormat.AMDesignator; + } + else if (type.Equals("hour")) + { + var shortTimePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern.ToLower(); + var resolvedLength = 1; + //valid Hours: h, hh + if (shortTimePattern.Contains("hh")) + { + resolvedLength = 2; + } + var hr = dt.Hour; + + if (Clock == "12HourClock") + hr = hr >= 13 ? hr - 12 : hr; + + if (len == -1) + return hr.ToString($"D{resolvedLength}"); + if (len < 1 || len > 2) + len = resolvedLength; + + return hr.ToString($"D{len}"); + } + else if (type.Equals("minute")) + { + var shortTimePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern.ToLower(); + var resolvedLength = 1; + //valid mintues: m, mm + if (shortTimePattern.Contains("hh")) + { + resolvedLength = 2; + } + + var dy = dt.Minute; + if (len == -1) + return dy.ToString($"D{resolvedLength}"); + if (len < 1 || len > 2) + len = resolvedLength; + + return dy.ToString($"D{len}"); + } + else if (type.Equals("second")) + { + var shortTimePattern = CultureInfo.CurrentCulture.DateTimeFormat.LongTimePattern.ToLower(); + var resolvedLength = 1; + //valid seconds: s, ss + if (shortTimePattern.Contains("ss")) + { + resolvedLength = 2; + } + + var dy = dt.Second; + if (len == -1) + return dy.ToString($"D{resolvedLength}"); + if (len < 1 || len > 2) + len = resolvedLength; + + return dy.ToString($"D{len}"); + } + else if (type.Equals("timezone")) + { + //Timezones aren't supported yet, that's a HUGE task + //Multiple timezones can exist for a given offset from UTC, + //So info about region is necessary to obtain it + //It'd be nice if MS would move stuff from WinRT globalization namespace to .net + throw new NotSupportedException("Timezones aren't supported yet"); + } + + throw new ArgumentException("Invalid format"); + + } + + private string GetFormatValue(string pattern, TimeSpan ts) + { + var sp = pattern.Split(new[] { "." }, StringSplitOptions.None); + var type = sp[0].Trim(); + var desc = sp.Count() > 1 ? sp[1].Trim() : ""; + var len = desc.Contains("(") ? int.Parse(desc.Substring(desc.IndexOf("(") + 1, desc.Length - desc.IndexOf(")"))) : -1; + + if (type.Equals("hour")) + { + var shortTimePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern.ToLower(); + var resolvedLength = 1; + //valid Hours: h, hh + if (shortTimePattern.Contains("hh")) + { + resolvedLength = 2; + } + var hr = ts.Hours; + + if (Clock == "12HourClock") + hr = hr >= 13 ? hr - 12 : hr; + + if (len == -1) + return hr.ToString($"D{resolvedLength}"); + if (len < 1 || len > 2) + len = resolvedLength; + + return hr.ToString($"D{len}"); + } + else if (type.Equals("minute")) + { + var shortTimePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern.ToLower(); + var resolvedLength = 1; + //valid mintues: m, mm + if (shortTimePattern.Contains("hh")) + { + resolvedLength = 2; + } + + var dy = ts.Minutes; + if (len == -1) + return dy.ToString($"D{resolvedLength}"); + if (len < 1 || len > 2) + len = resolvedLength; + + return dy.ToString($"D{len}"); + } + else if (type.Equals("second")) + { + var shortTimePattern = CultureInfo.CurrentCulture.DateTimeFormat.LongTimePattern.ToLower(); + var resolvedLength = 1; + //valid seconds: s, ss + if (shortTimePattern.Contains("ss")) + { + resolvedLength = 2; + } + + var dy = ts.Seconds; + if (len == -1) + return dy.ToString($"D{resolvedLength}"); + if (len < 1 || len > 2) + len = resolvedLength; + + return dy.ToString($"D{len}"); + } + else if (type.Equals("period")) + { + if (Clock == "24HourClock") + return ""; + + var hr = ts.Hours; + if (hr >= 12) + return CultureInfo.CurrentCulture.DateTimeFormat.PMDesignator; + else + return CultureInfo.CurrentCulture.DateTimeFormat.AMDesignator; + } + + throw new ArgumentException("Invalid format"); + + } + + private string GetFormatTemplate(DateTimeOffset dt) + { + switch (_format) + { + case "longdate": + return dt.ToString(CultureInfo.CurrentCulture.DateTimeFormat.LongDatePattern); + case "shortdate": + return dt.ToString(CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern); + case "longtime": + return dt.ToString(CultureInfo.CurrentCulture.DateTimeFormat.LongTimePattern); + case "shorttime": + return dt.ToString(CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern); + case "iso8601": + case "sortable": + return dt.ToString(CultureInfo.CurrentCulture.DateTimeFormat.SortableDateTimePattern); + case "universalsortable": + return dt.ToString(CultureInfo.CurrentCulture.DateTimeFormat.UniversalSortableDateTimePattern); + case "rfc1123": + return dt.ToUniversalTime().ToString(CultureInfo.CurrentCulture.DateTimeFormat.RFC1123Pattern); + } + throw new ArgumentException("Invalid template"); + } + + private string _format; + private string _clock; + } +}