1 changed files with 335 additions and 0 deletions
@ -0,0 +1,335 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using System.Text.RegularExpressions; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// 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
|
|||
/// <para>
|
|||
/// 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
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// /// 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
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// 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
|
|||
/// </para>
|
|||
/// </summary>
|
|||
public sealed class DateTimeFormatter |
|||
{ |
|||
public DateTimeFormatter(string formatString) |
|||
{ |
|||
_format = formatString; |
|||
var reg = new Regex("{([^{}]*)}"); |
|||
var results = reg.Matches(formatString).Cast<Match>().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<string> Formats { get; } |
|||
|
|||
/// <summary>
|
|||
/// Formats a DateTimeOffset object by the format of the <see cref="DateTimeFormatter"/>
|
|||
/// </summary>
|
|||
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; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Formats a TimeSpan object by the format of the <see cref="DateTimeFormatter"/>
|
|||
/// </summary>
|
|||
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; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue