mirror of
https://gitee.com/ant-design-blazor/ant-design-blazor.git
synced 2024-12-15 01:11:52 +08:00
b27e4d47a6
* fix(module:datepicker): validate manually entered date against format * fix(module:datepicker): keep frozen the panel until valid date entered * fix(module:datepicker): switch to current culture format * fix(module:datepicker): build fix * fix(module:datepicker): use InvariantCulture when calling ToString on date * fix(module:datepicker): FormatAnalyzer handles also year, week, month & quarter picker * fix(module:datepicker): FormatAnalyzer broken tests * fix: FormatAnalyzer handles prefixes in format. Suffix is handled for picker type = "year" Tests include validation of not changing state * fix: all modes go through format analyzing (inluding week&quarter) * fix(module:datepickerbase): InternalFormat initalized properly for all modes * fix(module:rangepicker): reset opposing date when in conflict with current * fix(module:rangePicker): handle null in second part of range * fix(module:datepicker): switch from BindConverter to partials * tests(module:datepicker): FormatAnalyzer new tests to cover switch from BindConverter * tests(module:datepicker): missed change in tests * fix: focus, key events, reset value to original if not confirmed * fix: bug fix on range & clean-up * Update DatePicker.razor Co-authored-by: James Yeung <shunjiey@hotmail.com>
358 lines
14 KiB
C#
358 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using AntDesign.core.Extensions;
|
|
using Microsoft.AspNetCore.Components;
|
|
|
|
namespace AntDesign.Datepicker.Locale
|
|
{
|
|
public class FormatAnalyzer
|
|
{
|
|
private int _yearLength;
|
|
private int _monthLength;
|
|
private int _dayLength;
|
|
private int _hourLength;
|
|
private int _minuteLength;
|
|
private int _secondLength;
|
|
private int _formatLength;
|
|
|
|
private List<string> _separators = new();
|
|
private List<DateTimePartialType> _partialsOrder = new();
|
|
private Dictionary<DateTimePartialType, int> _parsedMap;
|
|
private readonly string _analyzerType;
|
|
private bool _hasPrefix;
|
|
private int _startPosition;
|
|
private int _separatorPrefixOffset;
|
|
private readonly DatePickerLocale _locale;
|
|
|
|
public enum DateTimePartialType
|
|
{
|
|
Nothing,
|
|
Second,
|
|
Minute,
|
|
Hour,
|
|
Day,
|
|
Month,
|
|
Year
|
|
}
|
|
|
|
public FormatAnalyzer(string format, string analyzerType, DatePickerLocale locale)
|
|
{
|
|
_formatLength = format.Length;
|
|
_analyzerType = analyzerType;
|
|
_locale = locale;
|
|
//Quarter and Week have individual appoaches, so no need to analyze format
|
|
//if (!(_analyzerType == DatePickerType.Quarter || _analyzerType == DatePickerType.Week))
|
|
AnalyzeFormat(format);
|
|
_format = format;
|
|
}
|
|
|
|
private void AnalyzeFormat(string format)
|
|
{
|
|
_parsedMap = new();
|
|
bool? inDate = null;
|
|
bool isLastSeparator = false;
|
|
int partialOrder = 0;
|
|
for (var i = 0; i < format.Length; i++)
|
|
{
|
|
if (format[i].IsIn('d', 'M', 'y'))
|
|
{
|
|
inDate = true;
|
|
isLastSeparator = false;
|
|
_ = format[i] switch
|
|
{
|
|
'y' => Increment(ref _yearLength, ref partialOrder, DateTimePartialType.Year),
|
|
'M' => Increment(ref _monthLength, ref partialOrder, DateTimePartialType.Month),
|
|
'd' => Increment(ref _dayLength, ref partialOrder, DateTimePartialType.Day),
|
|
_ => throw new ArgumentException("Character not covered")
|
|
};
|
|
}
|
|
else if (format[i].IsIn('h', 'H', 'm', 's'))
|
|
{
|
|
inDate = false;
|
|
isLastSeparator = false;
|
|
_ = format[i] switch
|
|
{
|
|
'h' => Increment(ref _hourLength, ref partialOrder, DateTimePartialType.Hour),
|
|
'H' => Increment(ref _hourLength, ref partialOrder, DateTimePartialType.Hour),
|
|
'm' => Increment(ref _minuteLength, ref partialOrder, DateTimePartialType.Minute),
|
|
's' => Increment(ref _secondLength, ref partialOrder, DateTimePartialType.Second),
|
|
_ => throw new ArgumentException("Character not covered")
|
|
};
|
|
}
|
|
else //separators
|
|
{
|
|
if (!isLastSeparator)
|
|
{
|
|
_separators.Add(format[i].ToString());
|
|
if (inDate is null)
|
|
_hasPrefix = true;
|
|
}
|
|
else
|
|
{
|
|
_separators[_separators.Count-1] += format[i];
|
|
}
|
|
isLastSeparator = true;
|
|
}
|
|
}
|
|
if (_hasPrefix)
|
|
{
|
|
_startPosition = _separators[0].Length;
|
|
_separatorPrefixOffset = _hasPrefix ? 1 : 0;
|
|
}
|
|
}
|
|
|
|
private bool Increment(ref int lengthValue, ref int partialOrder, DateTimePartialType partialType)
|
|
{
|
|
if (lengthValue == 0)
|
|
{
|
|
_parsedMap.Add(partialType, 0);
|
|
_partialsOrder.Add(partialType);
|
|
partialOrder++;
|
|
}
|
|
lengthValue++;
|
|
return true;
|
|
}
|
|
|
|
public bool IsFullString(string forEvaluation)
|
|
{
|
|
if (forEvaluation.Length < _formatLength)
|
|
return false;
|
|
|
|
int startPosition = _startPosition, endingPosition, parsed;
|
|
for (int i = 0; i < _partialsOrder.Count; i++)
|
|
{
|
|
if (i < (_separators.Count - _separatorPrefixOffset))
|
|
endingPosition = forEvaluation.IndexOf(_separators[i + _separatorPrefixOffset], startPosition);
|
|
else
|
|
endingPosition = forEvaluation.Length;
|
|
//handles situation when separator was removed from date
|
|
if (endingPosition < 0)
|
|
return false;
|
|
string partial = forEvaluation.Substring(startPosition, endingPosition - startPosition);
|
|
(int minLen, int maxLen) borders = _partialsOrder[i] switch
|
|
{
|
|
DateTimePartialType.Year => (minLen: _yearLength, maxLen: 4),
|
|
DateTimePartialType.Month => (minLen: _monthLength, maxLen: 2),
|
|
DateTimePartialType.Day => (minLen: _dayLength, maxLen: 2),
|
|
DateTimePartialType.Hour => (minLen: _hourLength, maxLen: 2),
|
|
DateTimePartialType.Minute => (minLen: _minuteLength, maxLen: 2),
|
|
DateTimePartialType.Second => (minLen: _secondLength, maxLen: 2),
|
|
_ => throw new ArgumentException("Partial not covered")
|
|
};
|
|
//check width of the partial
|
|
if (!(borders.minLen <= partial.Length && partial.Length <= borders.maxLen))
|
|
return false;
|
|
//check if partial is pars-able and grater than 0
|
|
if (int.TryParse(partial, out parsed))
|
|
{
|
|
if ((parsed <= 0 && _partialsOrder[i] >= DateTimePartialType.Day)
|
|
|| (parsed < 0 && _partialsOrder[i] < DateTimePartialType.Day))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
//check if all characters in partial are digits (exclude for example partial = "201 ")
|
|
if (partial.Count(c => char.IsDigit(c)) != partial.Length)
|
|
return false;
|
|
|
|
if (endingPosition < forEvaluation.Length)
|
|
startPosition = endingPosition + _separators[i + _separatorPrefixOffset].Length;
|
|
_parsedMap[_partialsOrder[i]] = parsed;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public (bool, DateTime) TryParseQuarterString(string forEvaluation,
|
|
string separator = "-", string quarterPrefix = "Q")
|
|
{
|
|
var arr = forEvaluation.Split(separator);
|
|
if (arr.Length != 2)
|
|
return (false, default);
|
|
|
|
if (!ExtractYearPartial(forEvaluation, arr[0], out int year))
|
|
return (false, default);
|
|
|
|
if (!arr[1].StartsWith(quarterPrefix.ToUpper()) && !arr[1].StartsWith(quarterPrefix.ToLower()))
|
|
return (false, default);
|
|
|
|
string quarterAsString = arr[1].Substring(quarterPrefix.Length).Trim();
|
|
if (quarterAsString.Length == 1
|
|
&& int.TryParse(quarterAsString, out int quarter)
|
|
&& quarter > 0 && quarter <= 4)
|
|
{
|
|
//pick first day/month of the quarter
|
|
return (true, new DateTime(year, quarter * 3 - 2, 1));
|
|
}
|
|
|
|
return (false, default);
|
|
}
|
|
|
|
public (bool, DateTime) TryParseWeekString(string forEvaluation, string separator = "-")
|
|
{
|
|
var arr = forEvaluation.Split(separator);
|
|
if (arr.Length != 2)
|
|
return (false, default);
|
|
|
|
if (!ExtractYearPartial(forEvaluation, arr[0], out int year))
|
|
return (false, default);
|
|
|
|
if (!arr[1].EndsWith(_locale.Lang.Week))
|
|
return (false, default);
|
|
|
|
string weekAsString = arr[1].Substring(0, arr[1].Length - _locale.Lang.Week.Length).Trim();
|
|
|
|
if (!(weekAsString.Length > 0 && weekAsString.Length <= 2
|
|
&& int.TryParse(weekAsString, out int week)
|
|
&& week > 0 && week < 55))
|
|
return (false, default);
|
|
|
|
//pick first day of the week
|
|
var resultDate = new DateTime(year, 1, 1).AddDays(week * 7 - 7);
|
|
if (week > 1)
|
|
{
|
|
int mondayOffset = (7 + (resultDate.DayOfWeek - DayOfWeek.Monday)) % 7;
|
|
resultDate = resultDate.AddDays(-1 * mondayOffset);
|
|
}
|
|
//cover scenario of 54 weeks when most of times years do not have 54 weeks
|
|
if (resultDate.Year == year)
|
|
return (true, resultDate);
|
|
|
|
return (false, default);
|
|
}
|
|
|
|
private bool ExtractYearPartial(string forEvaluation, string partial, out int year)
|
|
{
|
|
year = 0;
|
|
if (!(partial.Length >= _locale.Lang.YearFormat.Length))
|
|
return false;
|
|
|
|
int startPosition = _startPosition, endingPosition;
|
|
endingPosition = forEvaluation.IndexOf(_separators[_separatorPrefixOffset][0], startPosition);
|
|
string yearPartial = partial.Substring(startPosition, endingPosition - startPosition).Trim();
|
|
if (!(_yearLength <= yearPartial.Length && yearPartial.Length <= 4))
|
|
return false;
|
|
|
|
if (!int.TryParse(yearPartial, out year))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
Func<string, (bool, DateTime)> _converter;
|
|
private readonly string _format;
|
|
|
|
private Func<string, (bool, DateTime)> Converter
|
|
{
|
|
get
|
|
{
|
|
if (_converter is null)
|
|
{
|
|
switch (_analyzerType)
|
|
{
|
|
case DatePickerType.Year:
|
|
_converter = (pickerString) => TryParseYear(pickerString);
|
|
break;
|
|
case DatePickerType.Quarter:
|
|
_converter = (pickerString) => TryParseQuarterString(pickerString);
|
|
break;
|
|
case DatePickerType.Week:
|
|
_converter = (pickerString) => TryParseWeekString(pickerString);
|
|
break;
|
|
default:
|
|
_converter = (pickerString) => TryParseDate(pickerString);
|
|
break;
|
|
}
|
|
}
|
|
return _converter;
|
|
}
|
|
}
|
|
|
|
public bool TryPickerStringConvert<TValue>(string pickerString, out TValue changeValue, bool isDateTypeNullable)
|
|
{
|
|
var resultTuple = Converter(pickerString);
|
|
if (resultTuple.Item1)
|
|
{
|
|
return GetParsedValue(out changeValue, resultTuple.Item2, isDateTypeNullable);
|
|
}
|
|
changeValue = default;
|
|
return false;
|
|
}
|
|
|
|
private (bool, DateTime) TryParseDate(string pickerString)
|
|
{
|
|
if (IsFullString(pickerString))
|
|
{
|
|
return (TryBuildDateFromPartials(out DateTime result), result);
|
|
}
|
|
return (false, default);
|
|
}
|
|
|
|
private bool TryBuildDateFromPartials(out DateTime result)
|
|
{
|
|
result = default;
|
|
int year = 1, month = 1, day = 1, hour = 0, minute = 0, second = 0;
|
|
foreach (var item in _parsedMap)
|
|
{
|
|
switch (item.Key)
|
|
{
|
|
case DateTimePartialType.Year:
|
|
if (item.Value < 0)
|
|
return false;
|
|
year = item.Value;
|
|
break;
|
|
case DateTimePartialType.Month:
|
|
if (item.Value < 1 || item.Value > 12)
|
|
return false;
|
|
month = item.Value;
|
|
break;
|
|
case DateTimePartialType.Day:
|
|
if (item.Value < 1 || item.Value > 31)
|
|
return false;
|
|
day = item.Value;
|
|
break;
|
|
case DateTimePartialType.Hour:
|
|
if (item.Value < 0 || item.Value > 23)
|
|
return false;
|
|
hour = item.Value;
|
|
break;
|
|
case DateTimePartialType.Minute:
|
|
if (item.Value < 0 || item.Value > 59)
|
|
return false;
|
|
minute = item.Value;
|
|
break;
|
|
case DateTimePartialType.Second:
|
|
if (item.Value < 0 || item.Value > 59)
|
|
return false;
|
|
second = item.Value;
|
|
break;
|
|
}
|
|
}
|
|
if (DateTime.DaysInMonth(year, month) < day)
|
|
return false;
|
|
result = new DateTime(year, month, day, hour, minute, second);
|
|
return true;
|
|
}
|
|
|
|
public (bool, DateTime) TryParseYear(string pickerString)
|
|
{
|
|
if (IsFullString(pickerString))
|
|
{
|
|
return (true, new DateTime(_parsedMap[DateTimePartialType.Year], 1, 1));
|
|
}
|
|
return (false, default);
|
|
}
|
|
|
|
private bool GetParsedValue<TValue>(out TValue changeValue, DateTime foundDate, bool isDateTypeNullable)
|
|
{
|
|
if (isDateTypeNullable)
|
|
changeValue = DataConvertionExtensions.Convert<DateTime?, TValue>(new DateTime?(foundDate));
|
|
else
|
|
changeValue = DataConvertionExtensions.Convert<DateTime, TValue>(foundDate);
|
|
return true;
|
|
}
|
|
}
|
|
}
|