1 /*
2  *  "GEDKeeper", the personal genealogical database editor.
3  *  Copyright (C) 2009-2021 by Sergey V. Zhdanovskih.
4  *
5  *  This file is part of "GEDKeeper".
6  *
7  *  This program is free software: you can redistribute it and/or modify
8  *  it under the terms of the GNU General Public License as published by
9  *  the Free Software Foundation, either version 3 of the License, or
10  *  (at your option) any later version.
11  *
12  *  This program is distributed in the hope that it will be useful,
13  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  *  GNU General Public License for more details.
16  *
17  *  You should have received a copy of the GNU General Public License
18  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 using System;
22 using System.Collections.Generic;
23 using BSLib;
24 using BSLib.Calendar;
25 using GDModel.Providers.GEDCOM;
26 using GKCore;
27 using GKCore.Types;
28 
29 namespace GDModel
30 {
31     /// <summary>
32     /// Class to hold simple standard GEDCOM dates.
33     /// Note: Year cannot be used externally with negative values even for "BC",
34     /// because these dates there is a special property.
35     /// Dates of type "BC" should have a positive Year + the property YearBC.
36     /// </summary>
37     public class GDMDate : GDMCustomDate
38     {
39         public const int UNKNOWN_YEAR = -1;
40 
41         private GDMApproximated fApproximated;
42         private GDMCalendar fCalendar;
43         private byte fDay;
44         private byte fMonth;
45         private short fYear;
46         private bool fYearBC;
47         private string fYearModifier;
48         private UDN fUDN;
49 
50 
51         public GDMApproximated Approximated
52         {
53             get { return fApproximated; }
54             set { fApproximated = value; }
55         }
56 
57         public GDMCalendar DateCalendar
58         {
59             get { return fCalendar; }
60         }
61 
62         public byte Day
63         {
64             get { return fDay; }
65             set {
66                 fDay = value;
67                 DateChanged();
68             }
69         }
70 
71         public byte Month
72         {
73             get { return fMonth; }
74             set {
75                 fMonth = value;
76                 DateChanged();
77             }
78         }
79 
80         public short Year
81         {
82             get { return fYear; }
83             set {
84                 fYear = value;
85                 DateChanged();
86             }
87         }
88 
89         public bool YearBC
90         {
91             get { return fYearBC; }
92             set {
93                 fYearBC = value;
94                 DateChanged();
95             }
96         }
97 
98         public string YearModifier
99         {
100             get { return fYearModifier; }
101             set { fYearModifier = value; }
102         }
103 
104 
GDMDate()105         public GDMDate()
106         {
107             fApproximated = GDMApproximated.daExact;
108             fCalendar = GDMCalendar.dcGregorian;
109             fYear = UNKNOWN_YEAR;
110             fYearBC = false;
111             fYearModifier = string.Empty;
112             fMonth = 0;
113             fDay = 0;
114         }
115 
GDMDate(int tagId, string tagValue)116         public GDMDate(int tagId, string tagValue) : this()
117         {
118             SetNameValue(tagId, tagValue);
119         }
120 
Clear()121         public override void Clear()
122         {
123             base.Clear();
124 
125             fApproximated = GDMApproximated.daExact;
126             fCalendar = GDMCalendar.dcGregorian;
127             fYear = UNKNOWN_YEAR;
128             fYearBC = false;
129             fYearModifier = string.Empty;
130             fMonth = 0;
131             fDay = 0;
132 
133             DateChanged();
134         }
135 
136         /// <summary>
137         /// This function is intended only for checking the completeness of parts of the date
138         /// (year, month and day are defined, are not unknown).
139         /// </summary>
IsValidDate()140         public bool IsValidDate()
141         {
142             return (fYear > 0 && fMonth > 0 && fDay > 0);
143         }
144 
IsEmpty()145         public override bool IsEmpty()
146         {
147             return base.IsEmpty() && fYear <= 0 && fMonth <= 0 && fDay <= 0;
148         }
149 
Assign(GDMTag source)150         public override void Assign(GDMTag source)
151         {
152             GDMDate srcDate = source as GDMDate;
153             if (srcDate == null)
154                 throw new ArgumentException(@"Argument is null or wrong type", "source");
155 
156             fApproximated = srcDate.fApproximated;
157             fCalendar = srcDate.fCalendar;
158             fYear = srcDate.fYear;
159             fYearBC = srcDate.fYearBC;
160             fYearModifier = srcDate.fYearModifier;
161             fMonth = srcDate.fMonth;
162             fDay = srcDate.fDay;
163 
164             DateChanged();
165         }
166 
GetDateTime()167         public override DateTime GetDateTime()
168         {
169             DateTime result;
170 
171             // FIXME: check if the calendar is gregorian
172             if (fYear >= 0 && fMonth >= 1 && fMonth <= 12 && fDay >= 1 && fDay <= 31) {
173                 result = new DateTime(fYear, fMonth, fDay);
174                 return result;
175             }
176 
177             result = new DateTime(0);
178             return result;
179         }
180 
SetDateTime(DateTime value)181         public override void SetDateTime(DateTime value)
182         {
183             SetGregorian(value.Day, value.Month, value.Year);
184         }
185 
ParseString(string strValue)186         public override string ParseString(string strValue)
187         {
188             string result;
189             if (string.IsNullOrEmpty(strValue)) {
190                 Clear();
191                 result = string.Empty;
192             } else {
193                 result = GEDCOMUtils.ParseDate(this, strValue);
194             }
195             return result;
196         }
197 
198         /// <summary>
199         /// Internal helper method for parser
200         /// </summary>
SetRawData(GDMApproximated approximated, GDMCalendar calendar, short year, bool yearBC, string yearModifier, byte month, byte day)201         internal void SetRawData(GDMApproximated approximated, GDMCalendar calendar,
202                                  short year, bool yearBC, string yearModifier, byte month, byte day)
203         {
204             fApproximated = approximated;
205             fCalendar = calendar;
206             fYear = year;
207             fYearBC = yearBC;
208             fYearModifier = yearModifier;
209             fMonth = month;
210             fDay = day;
211 
212             DateChanged();
213         }
214 
215         #region Private methods of parsing of the input format
216 
GetMonthNames(GDMCalendar calendar)217         public static string[] GetMonthNames(GDMCalendar calendar)
218         {
219             string[] monthes;
220             switch (calendar)
221             {
222                 case GDMCalendar.dcGregorian:
223                 case GDMCalendar.dcJulian:
224                 case GDMCalendar.dcRoman:
225                     monthes = GEDCOMConsts.GEDCOMMonthArray;
226                     break;
227 
228                 case GDMCalendar.dcHebrew:
229                     monthes = GEDCOMConsts.GEDCOMMonthHebrewArray;
230                     break;
231 
232                 case GDMCalendar.dcFrench:
233                     monthes = GEDCOMConsts.GEDCOMMonthFrenchArray;
234                     break;
235 
236                 case GDMCalendar.dcIslamic:
237                     monthes = GEDCOMConsts.GEDCOMMonthIslamicArray;
238                     break;
239 
240                 case GDMCalendar.dcUnknown:
241                 default:
242                     monthes = GEDCOMConsts.GEDCOMMonthArray;
243                     break;
244             }
245             return monthes;
246         }
247 
CheckGEDCOMMonth(GDMCalendar calendar, string str)248         private static string CheckGEDCOMMonth(GDMCalendar calendar, string str)
249         {
250             // An empty string is a valid identifier for an unknown month
251             if (string.IsNullOrEmpty(str)) return string.Empty;
252 
253             string[] monthes = GDMDate.GetMonthNames(calendar);
254             str = str.ToUpperInvariant();
255             for (int m = 0; m < monthes.Length; m++) {
256                 if (monthes[m] == str) {
257                     return str;
258                 }
259             }
260 
261             throw new GDMDateException("The string {0} is not a valid {1} month identifier", str, calendar.ToString());
262         }
263 
IntToGEDCOMMonth(int m)264         private static string IntToGEDCOMMonth(int m)
265         {
266             return (m == 0) ? string.Empty : GEDCOMConsts.GEDCOMMonthArray[m - 1];
267         }
268 
IntToGEDCOMMonthFrench(int m)269         private static string IntToGEDCOMMonthFrench(int m)
270         {
271             return (m == 0) ? string.Empty : GEDCOMConsts.GEDCOMMonthFrenchArray[m - 1];
272         }
273 
IntToGEDCOMMonthHebrew(int m)274         private static string IntToGEDCOMMonthHebrew(int m)
275         {
276             return (m == 0) ? string.Empty : GEDCOMConsts.GEDCOMMonthHebrewArray[m - 1];
277         }
278 
279         #endregion
280 
GetStringValue()281         protected override string GetStringValue()
282         {
283             var parts = new List<string>(5);
284             if (fApproximated != GDMApproximated.daExact) {
285                 parts.Add(GEDCOMConsts.GEDCOMDateApproximatedArray[(int)fApproximated]);
286             }
287 
288             if (fCalendar != GDMCalendar.dcGregorian) {
289                 parts.Add(GEDCOMConsts.GEDCOMDateEscapeArray[(int)fCalendar]);
290             }
291 
292             if (fDay > 0) {
293                 parts.Add(fDay.ToString("D2"));
294             }
295 
296             if (fMonth > 0) {
297                 string[] months = GetMonthNames(fCalendar);
298                 parts.Add(months[fMonth - 1]);
299             }
300 
301             if (fYear != UNKNOWN_YEAR) {
302                 string yearStr = fYear.ToString("D3");
303                 if (!string.IsNullOrEmpty(fYearModifier)) {
304                     yearStr = yearStr + "/" + fYearModifier;
305                 }
306 
307                 if (fYearBC) {
308                     yearStr += GEDCOMConsts.YearBC;
309                 }
310 
311                 parts.Add(yearStr);
312             }
313 
314             return string.Join(" ", parts);
315         }
316 
GetMonthNumber(GDMCalendar calendar, string strMonth)317         private static byte GetMonthNumber(GDMCalendar calendar, string strMonth)
318         {
319             string su = GEDCOMUtils.InvariantTextInfo.ToUpper(strMonth);
320 
321             int month;
322             switch (calendar) {
323                 case GDMCalendar.dcHebrew:
324                     month = Algorithms.IndexOf(GEDCOMConsts.GEDCOMMonthHebrewArray, su);
325                     break;
326 
327                 case GDMCalendar.dcFrench:
328                     month = Algorithms.IndexOf(GEDCOMConsts.GEDCOMMonthFrenchArray, su);
329                     break;
330 
331                 default:
332                     month = Algorithms.IndexOf(GEDCOMConsts.GEDCOMMonthArray, su);
333                     break;
334             }
335 
336             return (byte)(month + 1);
337         }
338 
SetDate(GDMCalendar calendar, int day, int month, int year, bool yearBC = false)339         public void SetDate(GDMCalendar calendar, int day, int month, int year, bool yearBC = false)
340         {
341             switch (calendar) {
342                 case GDMCalendar.dcGregorian:
343                     SetGregorian(day, month, year);
344                     break;
345 
346                 case GDMCalendar.dcJulian:
347                     SetJulian(day, month, year);
348                     break;
349 
350                 case GDMCalendar.dcHebrew:
351                     SetHebrew(day, month, year);
352                     break;
353 
354                 case GDMCalendar.dcFrench:
355                     SetFrench(day, month, year);
356                     break;
357 
358                 case GDMCalendar.dcRoman:
359                     SetRoman(day, month, year, yearBC);
360                     break;
361 
362                 case GDMCalendar.dcIslamic:
363                     SetIslamic(day, month, year);
364                     break;
365 
366                 case GDMCalendar.dcUnknown:
367                     SetUnknown(day, month, year, yearBC);
368                     break;
369             }
370         }
371 
SetDateInternal(GDMCalendar calendar, int day, string month, int year, string yearModifier, bool yearBC)372         private void SetDateInternal(GDMCalendar calendar, int day, string month, int year, string yearModifier, bool yearBC)
373         {
374             SetDateInternal(calendar, day, GetMonthNumber(calendar, month), year, yearModifier, yearBC);
375         }
376 
SetDateInternal(GDMCalendar calendar, int day, int month, int year, string yearModifier, bool yearBC)377         private void SetDateInternal(GDMCalendar calendar, int day, int month, int year, string yearModifier, bool yearBC)
378         {
379             fCalendar = calendar;
380             fDay = (byte)day;
381             fMonth = (byte)month;
382             fYear = (short)year;
383             fYearModifier = yearModifier;
384             fYearBC = yearBC;
385 
386             DateChanged();
387         }
388 
SetGregorian(int day, int month, int year)389         public void SetGregorian(int day, int month, int year)
390         {
391             SetDateInternal(GDMCalendar.dcGregorian, day, month, year, "", false);
392         }
393 
SetGregorian(int day, string month, int year, string yearModifier, bool yearBC)394         public void SetGregorian(int day, string month, int year, string yearModifier, bool yearBC)
395         {
396             SetDateInternal(GDMCalendar.dcGregorian, day, CheckGEDCOMMonth(GDMCalendar.dcGregorian, month), year, yearModifier, yearBC);
397         }
398 
SetJulian(int day, int month, int year)399         public void SetJulian(int day, int month, int year)
400         {
401             SetDateInternal(GDMCalendar.dcJulian, day, month, year, "", false);
402         }
403 
SetJulian(int day, string month, int year, bool yearBC)404         public void SetJulian(int day, string month, int year, bool yearBC)
405         {
406             SetDateInternal(GDMCalendar.dcJulian, day, CheckGEDCOMMonth(GDMCalendar.dcJulian, month), year, "", yearBC);
407         }
408 
SetHebrew(int day, int month, int year)409         public void SetHebrew(int day, int month, int year)
410         {
411             SetDateInternal(GDMCalendar.dcHebrew, day, month, year, "", false);
412         }
413 
SetHebrew(int day, string month, int year, bool yearBC)414         public void SetHebrew(int day, string month, int year, bool yearBC)
415         {
416             SetDateInternal(GDMCalendar.dcHebrew, day, CheckGEDCOMMonth(GDMCalendar.dcHebrew, month), year, "", yearBC);
417         }
418 
SetFrench(int day, int month, int year)419         public void SetFrench(int day, int month, int year)
420         {
421             SetDateInternal(GDMCalendar.dcFrench, day, month, year, "", false);
422         }
423 
SetFrench(int day, string month, int year, bool yearBC)424         public void SetFrench(int day, string month, int year, bool yearBC)
425         {
426             SetDateInternal(GDMCalendar.dcFrench, day, CheckGEDCOMMonth(GDMCalendar.dcFrench, month), year, "", yearBC);
427         }
428 
SetRoman(int day, int month, int year, bool yearBC)429         public void SetRoman(int day, int month, int year, bool yearBC)
430         {
431             SetDateInternal(GDMCalendar.dcRoman, day, month, year, "", yearBC);
432         }
433 
SetRoman(int day, string month, int year, bool yearBC)434         public void SetRoman(int day, string month, int year, bool yearBC)
435         {
436             SetDateInternal(GDMCalendar.dcRoman, day, CheckGEDCOMMonth(GDMCalendar.dcRoman, month), year, "", yearBC);
437         }
438 
SetUnknown(int day, int month, int year, bool yearBC)439         public void SetUnknown(int day, int month, int year, bool yearBC)
440         {
441             SetDateInternal(GDMCalendar.dcUnknown, day, month, year, "", yearBC);
442         }
443 
SetUnknown(int day, string month, int year, bool yearBC)444         public void SetUnknown(int day, string month, int year, bool yearBC)
445         {
446             SetDateInternal(GDMCalendar.dcUnknown, day, CheckGEDCOMMonth(GDMCalendar.dcUnknown, month), year, "", yearBC);
447         }
448 
SetIslamic(int day, int month, int year)449         public void SetIslamic(int day, int month, int year)
450         {
451             SetDateInternal(GDMCalendar.dcIslamic, day, month, year, "", false);
452         }
453 
SetIslamic(int day, string month, int year)454         public void SetIslamic(int day, string month, int year)
455         {
456             SetDateInternal(GDMCalendar.dcIslamic, day, CheckGEDCOMMonth(GDMCalendar.dcIslamic, month), year, "", false);
457         }
458 
459         #region UDN processing
460 
461         // GEDCOMCalendar: dcGregorian, dcJulian, dcHebrew, dcFrench, dcRoman, dcIslamic, dcUnknown.
462         private static readonly UDNCalendarType[] UDNCalendars = new UDNCalendarType[] {
463             /* dcGregorian */   UDNCalendarType.ctGregorian,
464             /* dcJulian */      UDNCalendarType.ctJulian,
465             /* dcHebrew */      UDNCalendarType.ctHebrew,
466             /* dcFrench */      UDNCalendarType.ctGregorian, // not supported yet
467             /* dcRoman */       UDNCalendarType.ctGregorian, // not supported yet
468             /* dcIslamic */     UDNCalendarType.ctIslamic,
469             /* dcUnknown */     UDNCalendarType.ctGregorian
470         };
471 
DateChanged()472         protected override void DateChanged()
473         {
474             int year = fYear;
475             if (year == UNKNOWN_YEAR) {
476                 year = UDN.UnknownYear;
477             } else {
478                 if (fYearBC) year = -year;
479             }
480 
481             UDNCalendarType udnCalendar = UDNCalendars[(int)fCalendar];
482             fUDN = new UDN(udnCalendar, year, fMonth, fDay);
483         }
484 
GetUDN()485         public override UDN GetUDN()
486         {
487             return (fApproximated == GDMApproximated.daExact) ? fUDN : UDN.CreateApproximate(fUDN);
488         }
489 
490         #endregion
491 
492         #region Utilities
493 
CreateByFormattedStr(string strDate, bool aException)494         public static GDMDate CreateByFormattedStr(string strDate, bool aException)
495         {
496             return CreateByFormattedStr(strDate, GDMCalendar.dcGregorian, aException);
497         }
498 
499         /// <summary>
500         /// This function transforms the string into a date. All components of
501         /// the date's string must be given by numbers in order of day / month / year.
502         /// This function is intended only for use with the date entry controls (fixed format of date's string).
503         /// </summary>
CreateByFormattedStr(string dateStr, GDMCalendar calendar, bool aException)504         public static GDMDate CreateByFormattedStr(string dateStr, GDMCalendar calendar, bool aException)
505         {
506             if (string.IsNullOrEmpty(dateStr)) return null;
507 
508             if (dateStr.IndexOf("-") >= 0) dateStr = dateStr.Replace("-", ".");
509             if (dateStr.IndexOf("/") >= 0) dateStr = dateStr.Replace("/", ".");
510             if (dateStr.IndexOf("_") >= 0) dateStr = dateStr.Replace("_", " ");
511 
512             string[] dtParts = dateStr.Split('.');
513             if (dtParts.Length < 3) {
514                 if (aException) {
515                     throw new GDMDateException("Invalid date format '{0}'", dateStr);
516                 }
517 
518                 return null;
519             }
520 
521             string pd = dtParts[0].Trim();
522             string pm = dtParts[1].Trim();
523             string py = dtParts[2].Trim();
524 
525             int day = (pd == "") ? 0 : ConvertHelper.ParseInt(pd, 0);
526             int month = (pm == "") ? 0 : ConvertHelper.ParseInt(pm, 0);
527             int year = (py == "") ? UNKNOWN_YEAR : ConvertHelper.ParseInt(py, UNKNOWN_YEAR);
528 
529             var date = new GDMDate();
530             date.SetDate(calendar, day, month, year);
531             return date;
532         }
533 
GetUDNByFormattedStr(string dateStr, GDMCalendar calendar, bool aException = false)534         public static UDN GetUDNByFormattedStr(string dateStr, GDMCalendar calendar, bool aException = false)
535         {
536             GDMDate dtx = GDMDate.CreateByFormattedStr(dateStr, calendar, aException);
537             return (dtx != null) ? dtx.GetUDN() : UDN.CreateEmpty();
538         }
539 
GetDisplayString(DateFormat format, bool includeBC = false, bool showCalendar = false)540         public string GetDisplayString(DateFormat format, bool includeBC = false, bool showCalendar = false)
541         {
542             string result = "";
543 
544             int year = fYear;
545             int month = fMonth;
546             int day = fDay;
547             bool ybc = fYearBC;
548 
549             if (year > 0 || month > 0 || day > 0) {
550                 switch (format) {
551                     case DateFormat.dfDD_MM_YYYY:
552                         result += day > 0 ? ConvertHelper.AdjustNumber(day, 2) + "." : "__.";
553                         result += month > 0 ? ConvertHelper.AdjustNumber(month, 2) + "." : "__.";
554                         result += year > 0 ? year.ToString().PadLeft(4, '_') : "____";
555                         break;
556 
557                     case DateFormat.dfYYYY_MM_DD:
558                         result += year > 0 ? year.ToString().PadLeft(4, '_') + "." : "____.";
559                         result += month > 0 ? ConvertHelper.AdjustNumber(month, 2) + "." : "__.";
560                         result += day > 0 ? ConvertHelper.AdjustNumber(day, 2) : "__";
561                         break;
562 
563                     case DateFormat.dfYYYY:
564                         if (year > 0) {
565                             result = year.ToString().PadLeft(4, '_');
566                         }
567                         break;
568                 }
569             }
570 
571             if (includeBC && ybc) {
572                 switch (format) {
573                     case DateFormat.dfDD_MM_YYYY:
574                         result = result + " BC";
575                         break;
576                     case DateFormat.dfYYYY_MM_DD:
577                         result = "BC " + result;
578                         break;
579                     case DateFormat.dfYYYY:
580                         result = "BC " + result;
581                         break;
582                 }
583             }
584 
585             if (showCalendar) {
586                 result = result + GKData.DateCalendars[(int)fCalendar].Sign;
587             }
588 
589             return result;
590         }
591 
GetDisplayStringExt(DateFormat format, bool sign, bool showCalendar)592         public override string GetDisplayStringExt(DateFormat format, bool sign, bool showCalendar)
593         {
594             string result = GetDisplayString(format, true, showCalendar);
595             if (sign && fApproximated != GDMApproximated.daExact) {
596                 result = "~ " + result;
597             }
598 
599             return result;
600         }
601 
602         #endregion
603     }
604 }
605