diff --git a/Terminal.Gui/Views/DateField.cs b/Terminal.Gui/Views/DateField.cs index 389911769d..4853ba9a1b 100644 --- a/Terminal.Gui/Views/DateField.cs +++ b/Terminal.Gui/Views/DateField.cs @@ -19,10 +19,14 @@ namespace Terminal.Gui; /// The provides date editing functionality with mouse support. /// public class DateField : TextField { + + private const string RIGHT_TO_LEFT_MARK = "\u200f"; + DateTime _date; - int _fieldLen = 10; - string _sepChar; - string _format; + private string _separator; + private string _format; + private readonly int _dateFieldLength = 12; + private int FormatLength => StandardizeDateFormat (_format).Trim ().Length; /// /// DateChanged event, raised when the property has changed. @@ -46,15 +50,14 @@ public DateField () : this (DateTime.MinValue) { } /// public DateField (DateTime date) : base ("") { - Width = _fieldLen + 2; + Width = _dateFieldLength; SetInitialProperties (date); } void SetInitialProperties (DateTime date) { - var cultureInfo = CultureInfo.CurrentCulture; - _sepChar = cultureInfo.DateTimeFormat.DateSeparator; - _format = $" {cultureInfo.DateTimeFormat.ShortDatePattern}"; + _format = $" {StandardizeDateFormat (Culture.DateTimeFormat.ShortDatePattern)}"; + _separator = GetDataSeparator (Culture.DateTimeFormat.DateSeparator); Date = date; CursorPosition = 1; TextChanging += DateField_Changing; @@ -111,8 +114,6 @@ public override bool OnProcessKeyDown (Key a) void DateField_Changing (object sender, TextChangingEventArgs e) { try { - var cultureInfo = CultureInfo.CurrentCulture; - DateTimeFormatInfo ccFmt = cultureInfo.DateTimeFormat; int spaces = 0; for (int i = 0; i < e.NewText.Length; i++) { if (e.NewText [i] == ' ') { @@ -121,13 +122,13 @@ void DateField_Changing (object sender, TextChangingEventArgs e) break; } } - spaces += _fieldLen; + spaces += FormatLength; string trimedText = e.NewText [..spaces]; - spaces -= _fieldLen; + spaces -= FormatLength; trimedText = trimedText.Replace (new string (' ', spaces), " "); - var date = Convert.ToDateTime (trimedText, ccFmt).ToString (ccFmt.ShortDatePattern); + var date = Convert.ToDateTime (trimedText).ToString (_format.Trim ()); if ($" {date}" != e.NewText) { - e.NewText = $" {date}"; + e.NewText = $" {date}".Replace (RIGHT_TO_LEFT_MARK, ""); } AdjCursorPosition (CursorPosition, true); } catch (Exception) { @@ -149,7 +150,8 @@ public DateTime Date { var oldData = _date; _date = value; - Text = value.ToString (_format); + Text = value.ToString (" " + StandardizeDateFormat (_format.Trim ())) + .Replace (RIGHT_TO_LEFT_MARK, ""); var args = new DateTimeEventArgs (oldData, value, _format); if (oldData != value) { OnDateChanged (args); @@ -157,16 +159,31 @@ public DateTime Date { } } + /// + /// CultureInfo for date. The default is CultureInfo.CurrentCulture. + /// + public CultureInfo Culture { + get => CultureInfo.CurrentCulture; + set { + if (value is not null) { + CultureInfo.CurrentCulture = value; + _separator = GetDataSeparator (value.DateTimeFormat.DateSeparator); + _format = " " + StandardizeDateFormat (value.DateTimeFormat.ShortDatePattern); + Text = Date.ToString (_format).Replace (RIGHT_TO_LEFT_MARK, ""); + } + } + } + /// public override int CursorPosition { get => base.CursorPosition; - set => base.CursorPosition = Math.Max (Math.Min (value, _fieldLen), 1); + set => base.CursorPosition = Math.Max (Math.Min (value, FormatLength), 1); } bool SetText (Rune key) { - if (CursorPosition > _fieldLen) { - CursorPosition = _fieldLen; + if (CursorPosition > FormatLength) { + CursorPosition = FormatLength; return false; } else if (CursorPosition < 1) { CursorPosition = 1; @@ -176,7 +193,7 @@ bool SetText (Rune key) var text = Text.EnumerateRunes ().ToList (); var newText = text.GetRange (0, CursorPosition); newText.Add (key); - if (CursorPosition < _fieldLen) { + if (CursorPosition < FormatLength) { newText = [.. newText, .. text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))]; } return SetText (StringExtensions.ToString (newText)); @@ -189,8 +206,13 @@ bool SetText (string text) } text = NormalizeFormat (text); - string [] vals = text.Split (_sepChar); - string [] frm = _format.Split (_sepChar); + string [] vals = text.Split (_separator); + for (var i = 0; i < vals.Length; i++) { + if (vals [i].Contains (RIGHT_TO_LEFT_MARK)) { + vals [i] = vals [i].Replace (RIGHT_TO_LEFT_MARK, ""); + } + } + string [] frm = _format.Split (_separator); int year; int month; int day; @@ -223,10 +245,13 @@ bool SetText (string text) } string d = GetDate (month, day, year, frm); - if (!DateTime.TryParseExact (d, _format, CultureInfo.CurrentCulture, DateTimeStyles.None, out var result)) { + DateTime date; + try { + date = Convert.ToDateTime (d); + } catch (Exception) { return false; } - Date = result; + Date = date; return true; } @@ -236,7 +261,7 @@ string NormalizeFormat (string text, string fmt = null, string sepChar = null) fmt = _format; } if (string.IsNullOrEmpty (sepChar)) { - sepChar = _sepChar; + sepChar = _separator; } if (fmt.Length != text.Length) { return text; @@ -265,7 +290,7 @@ string GetDate (int month, int day, int year, string [] fm) date += $"{year,4:0000}"; } if (i < 2) { - date += $"{_sepChar}"; + date += $"{_separator}"; } } return date; @@ -283,10 +308,57 @@ static int GetFormatIndex (string [] fm, string t) return idx; } + private string GetDataSeparator (string separator) + { + var sepChar = separator.Trim (); + if (sepChar.Length > 1 && sepChar.Contains (RIGHT_TO_LEFT_MARK)) { + sepChar = sepChar.Replace (RIGHT_TO_LEFT_MARK, ""); + } + + return sepChar; + } + + + + // Converts various date formats to a uniform 10-character format. + // This aids in simplifying the handling of single-digit months and days, + // and reduces the number of distinct date formats to maintain. + private static string StandardizeDateFormat (string format) => + format switch { + "MM/dd/yyyy" => "MM/dd/yyyy", + "yyyy-MM-dd" => "yyyy-MM-dd", + "yyyy/MM/dd" => "yyyy/MM/dd", + "dd/MM/yyyy" => "dd/MM/yyyy", + "d?/M?/yyyy" => "dd/MM/yyyy", + "dd.MM.yyyy" => "dd.MM.yyyy", + "dd-MM-yyyy" => "dd-MM-yyyy", + "dd/MM yyyy" => "dd/MM/yyyy", + "d. M. yyyy" => "dd.MM.yyyy", + "yyyy.MM.dd" => "yyyy.MM.dd", + "g yyyy/M/d" => "yyyy/MM/dd", + "d/M/yyyy" => "dd/MM/yyyy", + "d?/M?/yyyy g" => "dd/MM/yyyy", + "d-M-yyyy" => "dd-MM-yyyy", + "d.MM.yyyy" => "dd.MM.yyyy", + "d.MM.yyyy '?'." => "dd.MM.yyyy", + "M/d/yyyy" => "MM/dd/yyyy", + "d. M. yyyy." => "dd.MM.yyyy", + "d.M.yyyy." => "dd.MM.yyyy", + "g yyyy-MM-dd" => "yyyy-MM-dd", + "d.M.yyyy" => "dd.MM.yyyy", + "d/MM/yyyy" => "dd/MM/yyyy", + "yyyy/M/d" => "yyyy/MM/dd", + "dd. MM. yyyy." => "dd.MM.yyyy", + "yyyy. MM. dd." => "yyyy.MM.dd", + "yyyy. M. d." => "yyyy.MM.dd", + "d. MM. yyyy" => "dd.MM.yyyy", + _ => "dd/MM/yyyy" + }; + void IncCursorPosition () { - if (CursorPosition >= _fieldLen) { - CursorPosition = _fieldLen; + if (CursorPosition >= FormatLength) { + CursorPosition = FormatLength; return; } CursorPosition++; @@ -306,8 +378,8 @@ void DecCursorPosition () void AdjCursorPosition (int point, bool increment = true) { var newPoint = point; - if (point > _fieldLen) { - newPoint = _fieldLen; + if (point > FormatLength) { + newPoint = FormatLength; } if (point < 1) { newPoint = 1; @@ -316,7 +388,7 @@ void AdjCursorPosition (int point, bool increment = true) CursorPosition = newPoint; } - while (Text [CursorPosition] == _sepChar [0]) { + while (Text [CursorPosition].ToString () == _separator) { if (increment) { CursorPosition++; } else { @@ -335,7 +407,7 @@ bool MoveRight () new bool MoveEnd () { ClearAllSelection (); - CursorPosition = _fieldLen; + CursorPosition = FormatLength; return true; } diff --git a/UnitTests/Views/DateFieldTests.cs b/UnitTests/Views/DateFieldTests.cs index 993362c79f..309ef62102 100644 --- a/UnitTests/Views/DateFieldTests.cs +++ b/UnitTests/Views/DateFieldTests.cs @@ -170,4 +170,78 @@ public void Using_Pt_Culture () Assert.Equal (4, df.CursorPosition); CultureInfo.CurrentCulture = cultureBackup; } + + [Fact] + public void Using_All_Culture_StandardizeDateFormat () + { + CultureInfo cultureBackup = CultureInfo.CurrentCulture; + + DateTime date = DateTime.Parse ("1/1/1971"); + foreach (var culture in CultureInfo.GetCultures (CultureTypes.AllCultures)) { + CultureInfo.CurrentCulture = culture; + var separator = culture.DateTimeFormat.DateSeparator.Trim (); + if (separator.Length > 1 && separator.Contains ('\u200f')) { + separator = separator.Replace ("\u200f", ""); + } + var format = culture.DateTimeFormat.ShortDatePattern; + DateField df = new DateField (date); + if ((!culture.TextInfo.IsRightToLeft || (culture.TextInfo.IsRightToLeft && !df.Text.Contains ('\u200f'))) + && (format.StartsWith ('d') || format.StartsWith ('M'))) { + + switch (culture.Name) { + case "ar-SA": + Assert.Equal ($" 04{separator}11{separator}1390", df.Text); + break; + case "th": + case "th-TH": + Assert.Equal ($" 01{separator}01{separator}2514", df.Text); + break; + default: + Assert.Equal ($" 01{separator}01{separator}1971", df.Text); + break; + } + } else if (culture.TextInfo.IsRightToLeft) { + if (df.Text.Contains ('\u200f')) { + // It's a Unicode Character (U+200F) - Right-to-Left Mark (RLM) + Assert.True (df.Text.Contains ('\u200f')); + switch (culture.Name) { + case "ar-SA": + Assert.Equal ($" 04‏{separator}11‏{separator}1390", df.Text); + break; + default: + Assert.Equal ($" 01‏{separator}01‏{separator}1971", df.Text); + break; + } + } else { + switch (culture.Name) { + case "ckb-IR": + case "fa": + case "fa-AF": + case "fa-IR": + case "lrc": + case "lrc-IR": + case "mzn": + case "mzn-IR": + case "ps": + case "ps-AF": + case "uz-Arab": + case "uz-Arab-AF": + Assert.Equal ($" 1349{separator}10{separator}11", df.Text); + break; + default: + Assert.Equal ($" 1971{separator}01{separator}01", df.Text); + break; + } + } + } else { + switch (culture.Name) { + default: + Assert.Equal ($" 1971{separator}01{separator}01", df.Text); + break; + } + } + } + + CultureInfo.CurrentCulture = cultureBackup; + } }