| | | 1 | | #region Usings |
| | | 2 | | using System.Diagnostics.CodeAnalysis; |
| | | 3 | | using System.Globalization; |
| | | 4 | | #endregion |
| | | 5 | | |
| | | 6 | | namespace ZhEaIsNsAaBn.Utilities; |
| | | 7 | | |
| | | 8 | | /// <summary> |
| | | 9 | | /// A date held in the Hijri (<see cref="HijriCalendar"/>) calendar, convertible to and from |
| | | 10 | | /// Gregorian <see cref="DateTime"/>, <see cref="DateOnly"/> and date strings. |
| | | 11 | | /// </summary> |
| | | 12 | | /// <remarks>Only dates whose year is between 1900 and 2100 are accepted.</remarks> |
| | | 13 | | public class HijriDate |
| | | 14 | | { |
| | | 15 | | private const int EndGreg = 2100; |
| | | 16 | | private const int StartGreg = 1900; |
| | | 17 | | |
| | | 18 | | private const string DefaultFormat = "dd-MM-yyy"; |
| | | 19 | | |
| | 11 | 20 | | private static readonly string[] _allFormats = |
| | 11 | 21 | | [ |
| | 11 | 22 | | "yyyy/MM/dd", |
| | 11 | 23 | | "yyyy/M/d", |
| | 11 | 24 | | "dd/MM/yyyy", |
| | 11 | 25 | | "d/M/yyyy", |
| | 11 | 26 | | "dd/M/yyyy", |
| | 11 | 27 | | "d/MM/yyyy", |
| | 11 | 28 | | "yyyy-MM-dd", |
| | 11 | 29 | | "yyyy-M-d", |
| | 11 | 30 | | "dd-MM-yyyy", |
| | 11 | 31 | | "d-M-yyyy", |
| | 11 | 32 | | "dd-M-yyyy", |
| | 11 | 33 | | "d-MM-yyyy", |
| | 11 | 34 | | "yyyy MM dd", |
| | 11 | 35 | | "yyyy M d", |
| | 11 | 36 | | "dd MM yyyy", |
| | 11 | 37 | | "d M yyyy", |
| | 11 | 38 | | "dd M yyyy", |
| | 11 | 39 | | "d MM yyyy" |
| | 11 | 40 | | ]; |
| | | 41 | | |
| | 112 | 42 | | private readonly CultureInfo _arCul = new( "ar-SA" ); |
| | | 43 | | private readonly DateTime _dateTime; |
| | 112 | 44 | | private readonly CultureInfo _enCul = new( "en-US" ); |
| | 112 | 45 | | private readonly GregorianCalendar _gregorian = new(); |
| | 112 | 46 | | private readonly HijriCalendar _hijri = new(); |
| | | 47 | | |
| | | 48 | | |
| | | 49 | | /// <summary>Creates a date by parsing a Hijri or Gregorian date string.</summary> |
| | | 50 | | /// <param name="stringDate">The date, e.g. <c>1446/05/20</c> or <c>20-05-1446</c>. Time parts are not accepted.</pa |
| | | 51 | | /// <param name="isHijri"><see langword="true"/> (default) if <paramref name="stringDate"/> is Hijri; <see langword= |
| | | 52 | | /// <exception cref="HijriDateException"><paramref name="stringDate"/> is empty, not in a supported format, or outsi |
| | 36 | 53 | | public HijriDate( string stringDate, bool isHijri = true ) : this() |
| | 36 | 54 | | => _dateTime = isHijri ? FromHijriString( stringDate ) : FromGregorianString( stringDate ); |
| | | 55 | | |
| | | 56 | | /// <summary>Creates a date from a Gregorian <see cref="DateTime"/>. The time of day is dropped.</summary> |
| | | 57 | | /// <param name="gregorianDate">The Gregorian date.</param> |
| | | 58 | | /// <exception cref="HijriDateException">The year of <paramref name="gregorianDate"/> is outside 1900–2100.</excepti |
| | 152 | 59 | | public HijriDate( DateTime gregorianDate ) : this() => _dateTime = FromGregorianDate( gregorianDate ); |
| | | 60 | | |
| | 112 | 61 | | private HijriDate() |
| | | 62 | | { |
| | 112 | 63 | | _arCul.DateTimeFormat.Calendar = _hijri; |
| | 112 | 64 | | _enCul.DateTimeFormat.Calendar = _gregorian; |
| | 112 | 65 | | } |
| | | 66 | | |
| | | 67 | | |
| | | 68 | | /// <summary>Checks whether a string is a valid Hijri date in a supported format and year range.</summary> |
| | | 69 | | /// <param name="hijriString">The string to check.</param> |
| | | 70 | | /// <returns><see langword="true"/> if it parses as a Hijri date between 1900 and 2100; otherwise <see langword="fal |
| | | 71 | | public static bool IsHijriDate( string? hijriString ) |
| | | 72 | | { |
| | 62 | 73 | | if ( string.IsNullOrEmpty( hijriString ) ) { return false; } |
| | | 74 | | |
| | | 75 | | |
| | 40 | 76 | | var arCul = new CultureInfo( "ar-SA" ); |
| | 40 | 77 | | arCul.DateTimeFormat.Calendar = new HijriCalendar(); |
| | | 78 | | |
| | | 79 | | |
| | 40 | 80 | | return DateTime.TryParseExact( hijriString, _allFormats, arCul.DateTimeFormat, DateTimeStyles.AllowWhiteSpaces, |
| | 40 | 81 | | && tempDate.Year is >= StartGreg and <= EndGreg; |
| | | 82 | | } |
| | | 83 | | |
| | | 84 | | /// <summary>Gets today's date.</summary> |
| | | 85 | | /// <returns>Today's date, taken from the local clock.</returns> |
| | 4 | 86 | | public static HijriDate Now() => DateTime.Now; |
| | | 87 | | |
| | | 88 | | |
| | | 89 | | /// <summary>Compares two dates for equality. Returns <see langword="false"/> if either is <see langword="null"/>.</ |
| | | 90 | | /// <param name="date1">The first date.</param> |
| | | 91 | | /// <param name="date2">The second date.</param> |
| | | 92 | | /// <returns><see langword="true"/> if both are set and equal.</returns> |
| | | 93 | | public static bool operator ==( HijriDate? date1, HijriDate? date2 ) |
| | 132 | 94 | | => date1 is not null && date2 is not null && date1.ToDateTime() == date2.ToDateTime(); |
| | | 95 | | |
| | | 96 | | /// <summary>Compares two dates for inequality.</summary> |
| | | 97 | | /// <param name="date1">The first date.</param> |
| | | 98 | | /// <param name="date2">The second date.</param> |
| | | 99 | | /// <returns>The opposite of <c>==</c>.</returns> |
| | 8 | 100 | | public static bool operator !=( HijriDate? date1, HijriDate? date2 ) => !( date1 == date2 ); |
| | | 101 | | |
| | | 102 | | /// <summary>Checks whether <paramref name="date1"/> is later than <paramref name="date2"/>.</summary> |
| | | 103 | | /// <param name="date1">The first date.</param> |
| | | 104 | | /// <param name="date2">The second date.</param> |
| | | 105 | | /// <returns><see langword="true"/> if <paramref name="date1"/> is later.</returns> |
| | 0 | 106 | | public static bool operator >( HijriDate date1, HijriDate date2 ) => date1.ToDateTime() > date2.ToDateTime(); |
| | | 107 | | |
| | | 108 | | /// <summary>Checks whether <paramref name="date1"/> is the same as or later than <paramref name="date2"/>.</summary |
| | | 109 | | /// <param name="date1">The first date.</param> |
| | | 110 | | /// <param name="date2">The second date.</param> |
| | | 111 | | /// <returns><see langword="true"/> if <paramref name="date1"/> is the same or later.</returns> |
| | 0 | 112 | | public static bool operator >=( HijriDate date1, HijriDate date2 ) => date1.ToDateTime() >= date2.ToDateTime(); |
| | | 113 | | |
| | | 114 | | /// <summary>Checks whether <paramref name="date1"/> is earlier than <paramref name="date2"/>.</summary> |
| | | 115 | | /// <param name="date1">The first date.</param> |
| | | 116 | | /// <param name="date2">The second date.</param> |
| | | 117 | | /// <returns><see langword="true"/> if <paramref name="date1"/> is earlier.</returns> |
| | 0 | 118 | | public static bool operator <( HijriDate date1, HijriDate date2 ) => date1.ToDateTime() < date2.ToDateTime(); |
| | | 119 | | |
| | | 120 | | /// <summary>Checks whether <paramref name="date1"/> is the same as or earlier than <paramref name="date2"/>.</summa |
| | | 121 | | /// <param name="date1">The first date.</param> |
| | | 122 | | /// <param name="date2">The second date.</param> |
| | | 123 | | /// <returns><see langword="true"/> if <paramref name="date1"/> is the same or earlier.</returns> |
| | 0 | 124 | | public static bool operator <=( HijriDate date1, HijriDate date2 ) => date1.ToDateTime() <= date2.ToDateTime(); |
| | | 125 | | |
| | | 126 | | /// <summary>Parses a Hijri date string.</summary> |
| | | 127 | | /// <param name="s">The Hijri date string.</param> |
| | | 128 | | /// <exception cref="HijriDateException"><paramref name="s"/> is not a valid Hijri date.</exception> |
| | 0 | 129 | | public static implicit operator HijriDate( string s ) => new( s ); |
| | | 130 | | |
| | | 131 | | /// <summary>Converts a Gregorian <see cref="DateTime"/>. The time of day is dropped.</summary> |
| | | 132 | | /// <param name="date">The Gregorian date.</param> |
| | | 133 | | /// <exception cref="HijriDateException">The year is outside 1900–2100.</exception> |
| | 4 | 134 | | public static implicit operator HijriDate( DateTime date ) => new( date ); |
| | | 135 | | |
| | | 136 | | /// <summary>Formats the date as a Hijri string (<c>dd-MM-yyy</c>).</summary> |
| | | 137 | | /// <param name="hijriDate">The date.</param> |
| | 0 | 138 | | public static implicit operator string( HijriDate hijriDate ) => hijriDate.ToString(); |
| | | 139 | | |
| | | 140 | | /// <summary>Converts to a <see cref="DateTime"/>; same as <see cref="ToDateTime"/>.</summary> |
| | | 141 | | /// <param name="hijriDate">The date.</param> |
| | 0 | 142 | | public static implicit operator DateTime( HijriDate hijriDate ) => hijriDate.ToDateTime(); |
| | | 143 | | |
| | | 144 | | |
| | | 145 | | /// <summary>Gets the date as a <see cref="DateOnly"/>, reading the stored parts in the Hijri calendar.</summary> |
| | | 146 | | /// <returns>The date.</returns> |
| | 0 | 147 | | public DateOnly ToDateOnly() => DateOnly.FromDateTime( ToDateTime() ); |
| | | 148 | | |
| | | 149 | | /// <summary>Gets the date as a <see cref="DateTime"/>, reading the stored parts in the Hijri calendar.</summary> |
| | | 150 | | /// <returns>The date.</returns> |
| | | 151 | | public DateTime ToDateTime() |
| | 64 | 152 | | => new( _dateTime.Year, _dateTime.Month, _dateTime.Day, _dateTime.Hour, _dateTime.Minute, _dateTime.Second, |
| | 64 | 153 | | _arCul.DateTimeFormat.Calendar ); |
| | | 154 | | |
| | | 155 | | /// <summary>Gets the date as a <see cref="DateOnly"/>, reading the stored parts in the Gregorian calendar.</summary |
| | | 156 | | /// <returns>The date.</returns> |
| | 0 | 157 | | public DateOnly ToGregorianDateOnly() => DateOnly.FromDateTime( ToGregorianDateTime() ); |
| | | 158 | | |
| | | 159 | | /// <summary>Gets the date as a <see cref="DateTime"/>, reading the stored parts in the Gregorian calendar.</summary |
| | | 160 | | /// <returns>The date.</returns> |
| | | 161 | | public DateTime ToGregorianDateTime() |
| | 12 | 162 | | => new( _dateTime.Year, _dateTime.Month, _dateTime.Day, _dateTime.Hour, _dateTime.Minute, _dateTime.Second, |
| | 12 | 163 | | _enCul.DateTimeFormat.Calendar ); |
| | | 164 | | |
| | | 165 | | /// <inheritdoc /> |
| | 44 | 166 | | public override bool Equals( object? obj ) => obj is HijriDate other && _dateTime == other._dateTime; |
| | | 167 | | |
| | | 168 | | /// <inheritdoc /> |
| | 8 | 169 | | public override int GetHashCode() => _dateTime.GetHashCode(); |
| | | 170 | | |
| | | 171 | | /// <summary>Formats the date as a Hijri string (<c>dd-MM-yyy</c>).</summary> |
| | | 172 | | /// <returns>The formatted date.</returns> |
| | 0 | 173 | | public override string ToString() => ToString( DefaultFormat ); |
| | | 174 | | |
| | | 175 | | /// <summary>Formats the date with the default format (<c>dd-MM-yyy</c>).</summary> |
| | | 176 | | /// <param name="toGregorian"><see langword="true"/> to format in the Gregorian calendar; <see langword="false"/> fo |
| | | 177 | | /// <returns>The formatted date.</returns> |
| | 0 | 178 | | public string ToString( bool toGregorian ) => ToString( DefaultFormat, toGregorian ); |
| | | 179 | | |
| | | 180 | | /// <summary>Formats the date with a custom format.</summary> |
| | | 181 | | /// <param name="format">A <see cref="DateTime"/> format string.</param> |
| | | 182 | | /// <param name="toGregorian"><see langword="true"/> to format in the Gregorian calendar; <see langword="false"/> (d |
| | | 183 | | /// <returns>The formatted date.</returns> |
| | | 184 | | /// <exception cref="FormatException"><paramref name="format"/> is not a valid format string.</exception> |
| | | 185 | | public string ToString( |
| | | 186 | | [ StringSyntax( StringSyntaxAttribute.DateTimeFormat ) ] string format, |
| | | 187 | | bool toGregorian = false |
| | | 188 | | ) |
| | 44 | 189 | | => toGregorian |
| | 44 | 190 | | ? _dateTime.ToString( format, _enCul.DateTimeFormat ) |
| | 44 | 191 | | : _dateTime.ToString( format, _arCul.DateTimeFormat ); |
| | | 192 | | |
| | | 193 | | |
| | | 194 | | private static DateTime FromGregorianDate( DateTime gregorianDate ) |
| | | 195 | | { |
| | 76 | 196 | | if ( gregorianDate.Year is < StartGreg or > EndGreg ) |
| | | 197 | | { |
| | 4 | 198 | | throw new HijriDateException( |
| | 4 | 199 | | $"Convert Gregorian date to Hijri date unsupported data : {gregorianDate.ToString( "yyyy-MM-dd", Cul |
| | 4 | 200 | | ); |
| | | 201 | | } |
| | | 202 | | |
| | | 203 | | |
| | 72 | 204 | | return gregorianDate.Date; |
| | | 205 | | } |
| | | 206 | | |
| | | 207 | | private DateTime FromGregorianString( string gregorianString ) |
| | 4 | 208 | | => ParseString( gregorianString, _enCul, "Convert Gregorian string to Hijri date" ); |
| | | 209 | | |
| | | 210 | | private DateTime FromHijriString( string hijriString ) |
| | 32 | 211 | | => ParseString( hijriString, _arCul, "Convert Hijri string to Hijri date" ); |
| | | 212 | | |
| | | 213 | | private static DateTime ParseString( string value, CultureInfo culture, string operation ) |
| | | 214 | | { |
| | 40 | 215 | | if ( string.IsNullOrEmpty( value ) ) { throw new HijriDateException( $"{operation} Error: Date String is Empty" |
| | | 216 | | |
| | | 217 | | |
| | 32 | 218 | | if ( !DateTime.TryParseExact( value, _allFormats, culture.DateTimeFormat, DateTimeStyles.AllowWhiteSpaces, out v |
| | | 219 | | { |
| | 4 | 220 | | throw new HijriDateException( $"{operation} Error : {value}" ); |
| | | 221 | | } |
| | | 222 | | |
| | | 223 | | |
| | 28 | 224 | | if ( date.Year is < StartGreg or > EndGreg ) { throw new HijriDateException( $"{operation} unsupported data : {v |
| | | 225 | | |
| | | 226 | | |
| | 28 | 227 | | return date; |
| | | 228 | | } |
| | | 229 | | } |