From 3c3853dab240ee7574b85bd73753c7e1c52e6610 Mon Sep 17 00:00:00 2001 From: amwx Date: Tue, 16 Jun 2020 16:56:51 -0500 Subject: [PATCH] Remove DateTimeFormatter/use normal .net formatting --- .../Pages/DateTimePickerPage.xaml | 4 +- .../DateTimePickers/DatePicker.cs | 12 +- .../DateTimePickers/DatePickerPresenter.cs | 22 +- .../DateTimePickers/DateTimeFormatter.cs | 335 ------------------ .../DateTimePickers/TimePicker.cs | 40 ++- .../DateTimePickers/TimePickerPresenter.cs | 22 +- 6 files changed, 55 insertions(+), 380 deletions(-) delete mode 100644 src/Avalonia.Controls/DateTimePickers/DateTimeFormatter.cs diff --git a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml index e2d14167e5..af6b6e8605 100644 --- a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml +++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml @@ -34,14 +34,14 @@ - - <DatePicker DayFormat="{}{day.integer} ({dayofweek.abbreviated})" YearVisible="False" /> + <DatePicker DayFormat="d (ddd)" YearVisible="False" /> diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs index e9bb742bb4..4edc8467d5 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -447,9 +447,9 @@ namespace Avalonia.Controls { PseudoClasses.Set(":hasnodate", false); var selDate = SelectedDate.Value; - _monthText.Text = new DateTimeFormatter(MonthFormat).Format(selDate); - _yearText.Text = new DateTimeFormatter(YearFormat).Format(selDate); - _dayText.Text = new DateTimeFormatter(DayFormat).Format(selDate); + _monthText.Text = selDate.ToString(MonthFormat); + _yearText.Text = selDate.ToString(YearFormat); + _dayText.Text = selDate.ToString(DayFormat); } else { @@ -504,13 +504,13 @@ namespace Avalonia.Controls private int _dayIndex; private int _yearIndex; - private string _dayFormat = "{day.integer}"; + private string _dayFormat = "%d"; private bool _dayVisible = true; private DateTimeOffset _maxYear; private DateTimeOffset _minYear; - private string _monthFormat = "{month.full}"; + private string _monthFormat = "MMMM"; private bool _monthVisible = true; - private string _yearFormat = "{year.full}"; + private string _yearFormat = "yyyy"; private bool _yearVisible = true; private DateTimeOffset? _selectedDate; } diff --git a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs index 0dbe047097..8c43c3f7a8 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs @@ -511,7 +511,6 @@ namespace Avalonia.Controls _dayItems.Clear(); var format = DayFormat; - DateTimeFormatter formatter = new DateTimeFormatter(format); var date = Date; GregorianCalendar gc = new GregorianCalendar(); var daysInMonth = gc.GetDaysInMonth(date.Date.Year, date.Date.Month); @@ -522,9 +521,8 @@ namespace Avalonia.Controls curDt = new DateTimeOffset(date.Date.Year, date.Date.Month, dayIndex, 0, 0, 0, date.Offset); DatePickerPresenterItem dppi = new DatePickerPresenterItem(curDt); - dppi.DisplayText = formatter.Format(curDt); + dppi.DisplayText = curDt.ToString(format); _dayItems.Add(dppi); - dayIndex++; } } @@ -541,7 +539,6 @@ namespace Avalonia.Controls _monthItems.Clear(); var format = MonthFormat; - DateTimeFormatter formatter = new DateTimeFormatter(format); var date = Date; int monthIndex = 1; DateTimeOffset curDt; @@ -550,7 +547,7 @@ namespace Avalonia.Controls curDt = new DateTimeOffset(date.Date.Year, monthIndex, 1, 0, 0, 0, date.Offset); DatePickerPresenterItem dppi = new DatePickerPresenterItem(curDt); - dppi.DisplayText = formatter.Format(curDt); + dppi.DisplayText = curDt.ToString(format); _monthItems.Add(dppi); monthIndex++; @@ -568,14 +565,13 @@ namespace Avalonia.Controls _yearItems.Clear(); var format = YearFormat; - DateTimeFormatter formatter = new DateTimeFormatter(format); var curDt = MinYear.Date; var max = MaxYear.Date; while (curDt <= max) { DatePickerPresenterItem dppi = new DatePickerPresenterItem(curDt); - dppi.DisplayText = formatter.Format(curDt); + dppi.DisplayText = curDt.ToString(format); _yearItems.Add(dppi); curDt = curDt.AddYears(1); } @@ -594,7 +590,7 @@ namespace Avalonia.Controls if (!forceRecreate && daysInMonth == _dayItems.Count) return; - DateTimeFormatter formatter = new DateTimeFormatter(DayFormat); + var format = DayFormat; if (forceRecreate) { //We're not actually going to recreate the entire list, we're just @@ -604,7 +600,7 @@ namespace Avalonia.Controls DateTimeOffset curDt = new DateTimeOffset(Date.Date.Year, Date.Date.Month, 1, 0, 0, 0, Date.Offset); for (int i = 0; i < _dayItems.Count; i++) { - _dayItems[i].UpdateStoredDate(curDt, formatter.Format(curDt)); + _dayItems[i].UpdateStoredDate(curDt, curDt.ToString(format)); curDt = curDt.AddDays(1); } } @@ -619,7 +615,7 @@ namespace Avalonia.Controls { curDt = new DateTimeOffset(date.Date.Year, date.Date.Month, dayIndex, 0, 0, 0, date.Offset); DatePickerPresenterItem dppi = new DatePickerPresenterItem(curDt); - dppi.DisplayText = formatter.Format(curDt); + dppi.DisplayText = curDt.ToString(format); _dayItems.Add(dppi); dayIndex++; } @@ -1100,13 +1096,13 @@ namespace Avalonia.Controls private LoopingSelector _daySelector; private DateTimeOffset _date; - private string _dayFormat = "{day.integer}"; + private string _dayFormat = "%d"; private bool _dayVisible = true; private DateTimeOffset _maxYear; private DateTimeOffset _minYear; - private string _monthFormat = "{month.full}"; + private string _monthFormat = "MMMM"; private bool _monthVisible = true; - private string _yearFormat = "{year.full}"; + private string _yearFormat = "yyyy"; private bool _yearVisible = true; private DateTimeOffset _initDate; diff --git a/src/Avalonia.Controls/DateTimePickers/DateTimeFormatter.cs b/src/Avalonia.Controls/DateTimePickers/DateTimeFormatter.cs deleted file mode 100644 index 6ba83dd30e..0000000000 --- a/src/Avalonia.Controls/DateTimePickers/DateTimeFormatter.cs +++ /dev/null @@ -1,335 +0,0 @@ -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 == 0 ? 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 == 0 ? 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; - } -} diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs index f29036a850..2c6010dc1a 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs @@ -204,15 +204,9 @@ namespace Avalonia.Controls var time = SelectedTime; if (time.HasValue) { - //DateTimeFormatter will automatically handle the correct format for - //hours/minutes based on culture settings - DateTimeFormatter formatter = new DateTimeFormatter("{hour}"); - formatter.Clock = ClockIdentifier; - _hourText.Text = formatter.Format(time.Value); - - formatter = new DateTimeFormatter("{minute}"); - formatter.Clock = ClockIdentifier; - _minuteText.Text = formatter.Format(time.Value); + _hourText.Text = GetTimeFormat(time.Value, true, ClockIdentifier); + + _minuteText.Text = GetTimeFormat(time.Value); PseudoClasses.Set(":hasnotime", false); if (time.Value.Hours >= 12) @@ -233,6 +227,34 @@ namespace Avalonia.Controls } } + /// + /// Helps formatting timespans for use in TimePicker and TimePickerPresenter + /// + /// Timespan to format + /// True if formatting hour, false if minute + /// + /// + internal static string GetTimeFormat(TimeSpan timeSpan, bool forHour = false, string clockIdentifier = "12HourClock") + { + var fmt = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern.ToLower(); + + if (forHour) + { + var hr = timeSpan.Hours; + + if (clockIdentifier == "12HourClock" && hr > 12) + hr -= 12; + else if (clockIdentifier == "12HourClock" && hr == 0) + hr = 12; + + return fmt.Contains("hh") ? hr.ToString("D2") : hr.ToString(); + } + else + { + return timeSpan.ToString("mm"); + } + } + private void OnFlyoutButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e) { _presenter.ClockIdentifier = ClockIdentifier; diff --git a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs index bb7ae30d02..4c5b0b035c 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs @@ -94,8 +94,6 @@ namespace Avalonia.Controls { var old = _Time; SetAndRaise(TimeProperty, ref _Time, value); - //SelectedTimeChanged?.Invoke(this, new TimePickerSelectedValueChangedEventArgs(old, value)); - //SetSelectedTimeText(); } } @@ -269,16 +267,14 @@ namespace Avalonia.Controls if (_hourItems == null) { _hourItems = new AvaloniaList(); - DateTimeFormatter formatter = new DateTimeFormatter("{hour}"); - formatter.Clock = clock; - + int numItems = clock == "12HourClock" ? 12 : 24; for (int i = 0; i < numItems; i++) { var hr = clock == "12HourClock" ? TimeSpan.FromHours(i + 1) : TimeSpan.FromHours(i); TimePickerPresenterItem tppi = new TimePickerPresenterItem(hr); - tppi.DisplayText = formatter.Format(hr); + tppi.DisplayText = TimePicker.GetTimeFormat(hr, true, clock); _hourItems.Add(tppi); } @@ -287,8 +283,6 @@ namespace Avalonia.Controls else if (_hasClockChanged) { _hourItems.Clear(); - DateTimeFormatter formatter = new DateTimeFormatter("{hour}"); - formatter.Clock = clock; int numItems = clock == "12HourClock" ? 12 : 24; @@ -296,7 +290,7 @@ namespace Avalonia.Controls { var hr = clock == "12HourClock" ? TimeSpan.FromHours(i + 1) : TimeSpan.FromHours(i); TimePickerPresenterItem tppi = new TimePickerPresenterItem(hr); - tppi.DisplayText = formatter.Format(hr); + tppi.DisplayText = TimePicker.GetTimeFormat(hr, true, clock); _hourItems.Add(tppi); } } @@ -320,14 +314,13 @@ namespace Avalonia.Controls if (_minuteItems == null) { _minuteItems = new AvaloniaList(); - DateTimeFormatter formatter = new DateTimeFormatter("{minute}"); - + var inc = MinuteIncrement; for (int i = 0; i < 60; i += inc) { var min = TimeSpan.FromMinutes(i); TimePickerPresenterItem tppi = new TimePickerPresenterItem(min); - tppi.DisplayText = formatter.Format(min); + tppi.DisplayText = TimePicker.GetTimeFormat(min); _minuteItems.Add(tppi); } @@ -336,14 +329,13 @@ namespace Avalonia.Controls else if (_hasMinuteIncChanged) { _minuteItems.Clear(); - DateTimeFormatter formatter = new DateTimeFormatter("{minute}"); - + var inc = MinuteIncrement; for (int i = 0; i < 60; i += inc) { var min = TimeSpan.FromMinutes(i); TimePickerPresenterItem tppi = new TimePickerPresenterItem(min); - tppi.DisplayText = formatter.Format(min); + tppi.DisplayText = TimePicker.GetTimeFormat(min); _minuteItems.Add(tppi); } }