From ed35e1de38219be81f4fdde40ff389654d45c1e0 Mon Sep 17 00:00:00 2001 From: Doug Blank Date: Sat, 17 Jan 2009 14:33:10 +0000 Subject: [PATCH] Bug fix #1834, 1842: new-year boundaries in history, and editing slash dates: These changes allow the date editor dialog to change slashdates, and to change the first day of the year from Jan1. This is important for some date calculations and orderings. In order to do this, a new date format variation has been added. You can put Mar1, Mar25, or Sept1 in the parens with or without a Calendar type. For example: 'Jan 1, 1735 (Julian,Mar25)'. See further docs in wiki. svn: r11644 --- src/DateEdit.py | 50 +++++++++++--- src/DateHandler/_DateDisplay.py | 42 +++++++++--- src/DateHandler/_DateParser.py | 76 ++++++++++++++++------ src/gen/lib/date.py | 52 ++++++++++++++- src/glade/gramps.glade | 112 ++++++++++++++++++++++++++++++-- 5 files changed, 285 insertions(+), 47 deletions(-) diff --git a/src/DateEdit.py b/src/DateEdit.py index d05bd861d..c6a289c23 100644 --- a/src/DateEdit.py +++ b/src/DateEdit.py @@ -2,6 +2,7 @@ # Gramps - a GTK+/GNOME based genealogy program # # Copyright (C) 2002-2006 Donald N. Allingham +# Copyright (C) 2009 Douglas S. Blank # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -248,10 +249,22 @@ class DateEditorDialog(ManagedWindow.ManagedWindow): self.start_year.set_sensitive(0) self.calendar_box.set_sensitive(0) self.quality_box.set_sensitive(0) + self.dual_dated.set_sensitive(0) + self.new_year.set_sensitive(0) self.text_entry = self.top.get_widget('date_text_entry') self.text_entry.set_text(self.date.get_text()) - + + self.dual_dated = self.top.get_widget('dualdated') + if self.date.get_slash(): + self.dual_dated.set_active(1) + self.calendar_box.set_sensitive(0) + self.calendar_box.set_active(Date.CAL_JULIAN) + self.dual_dated.connect('toggled', self.switch_dual_dated) + + self.new_year = self.top.get_widget('newyear') + self.new_year.set_active(self.date.get_new_year()) + # The dialog is modal -- since dates don't have names, we don't # want to have several open dialogs, since then the user will # loose track of which is which. Much like opening files. @@ -270,14 +283,15 @@ class DateEditorDialog(ManagedWindow.ManagedWindow): else: if response == gtk.RESPONSE_OK: (the_quality, the_modifier, the_calendar, - the_value, the_text) = self.build_date_from_ui() + the_value, the_text, the_newyear) = self.build_date_from_ui() self.return_date = Date(self.date) self.return_date.set( quality=the_quality, modifier=the_modifier, calendar=the_calendar, value=the_value, - text=the_text) + text=the_text, + newyear=the_newyear) self.close() break @@ -313,19 +327,20 @@ class DateEditorDialog(ManagedWindow.ManagedWindow): self.start_day.get_value_as_int(), self.start_month_box.get_active(), self.start_year.get_value_as_int(), - False, + self.dual_dated.get_active(), self.stop_day.get_value_as_int(), self.stop_month_box.get_active(), self.stop_year.get_value_as_int(), - False) + self.dual_dated.get_active()) else: value = ( self.start_day.get_value_as_int(), self.start_month_box.get_active(), self.start_year.get_value_as_int(), - False) + self.dual_dated.get_active()) calendar = self.calendar_box.get_active() - return (quality, modifier, calendar, value, text) + newyear = self.new_year.get_active() + return (quality, modifier, calendar, value, text, newyear) def switch_type(self, obj): """ @@ -352,6 +367,20 @@ class DateEditorDialog(ManagedWindow.ManagedWindow): self.start_year.set_sensitive(date_sensitivity) self.calendar_box.set_sensitive(date_sensitivity) self.quality_box.set_sensitive(date_sensitivity) + self.dual_dated.set_sensitive(date_sensitivity) + self.new_year.set_sensitive(date_sensitivity) + + def switch_dual_dated(self, obj): + """ + Changed whether this is a dual dated year, or not. + Dual dated years are represented in the Julian calendar + so that the day/months don't changed in the Text representation. + """ + if self.dual_dated.get_active(): + self.calendar_box.set_active(Date.CAL_JULIAN) + self.calendar_box.set_sensitive(0) + else: + self.calendar_box.set_sensitive(1) def switch_calendar(self, obj): """ @@ -362,14 +391,15 @@ class DateEditorDialog(ManagedWindow.ManagedWindow): old_cal = self.date.get_calendar() new_cal = self.calendar_box.get_active() - (the_quality, the_modifier, the_calendar, the_value, the_text) = \ - self.build_date_from_ui() + (the_quality, the_modifier, the_calendar, + the_value, the_text, the_newyear) = self.build_date_from_ui() self.date.set( quality=the_quality, modifier=the_modifier, calendar=old_cal, value=the_value, - text=the_text) + text=the_text, + newyear=the_newyear) if not self.date.is_empty(): self.date.convert_calendar(new_cal) diff --git a/src/DateHandler/_DateDisplay.py b/src/DateHandler/_DateDisplay.py index 0150ef430..a47a66416 100644 --- a/src/DateHandler/_DateDisplay.py +++ b/src/DateHandler/_DateDisplay.py @@ -91,9 +91,11 @@ class DateDisplay: formats = ("YYYY-MM-DD (ISO)", ) calendar = ( - "", " (Julian)", " (Hebrew)", " (French Republican)", - " (Persian)", " (Islamic)" + "", "Julian", "Hebrew", "French Republican", + "Persian", "Islamic" ) + + newyear = ("", "Mar1", "Mar25", "Sep1") _mod_str = ("", "before ", "after ", "about ", "", "", "") @@ -133,6 +135,22 @@ class DateDisplay: else: return self.display(date) + def format_extras(self, cal, newyear): + """ + Formats the extra items (calendar, newyear) for a date. + """ + scal = self.calendar[cal] + snewyear = self.newyear[newyear] + retval = "" + for item in [scal, snewyear]: + if item: + if retval: + retval += "," + retval += item + if retval: + return " (%s)" % retval + return "" + def display(self, date): """ Return a text string representing the date. @@ -141,6 +159,7 @@ class DateDisplay: cal = date.get_calendar() qual = date.get_quality() start = date.get_start_date() + newyear = date.get_new_year() qual_str = self._qual_str[qual] @@ -151,11 +170,12 @@ class DateDisplay: elif mod == Date.MOD_SPAN or mod == Date.MOD_RANGE: d1 = self.display_iso(start) d2 = self.display_iso(date.get_stop_date()) - return "%s %s - %s%s" % (qual_str, d1, d2, self.calendar[cal]) + scal = self.format_extras(cal, newyear) + return "%s %s - %s%s" % (qual_str, d1, d2, scal) else: text = self.display_iso(start) - return "%s%s%s%s" % (qual_str, self._mod_str[mod], text, - self.calendar[cal]) + scal = self.format_extras(cal, newyear) + return "%s%s%s%s" % (qual_str, self._mod_str[mod], text, scal) def _slash_year(self, val, slash): if val < 0: @@ -328,6 +348,7 @@ class DateDisplayEn(DateDisplay): cal = date.get_calendar() qual = date.get_quality() start = date.get_start_date() + newyear = date.get_new_year() qual_str = self._qual_str[qual] @@ -338,13 +359,14 @@ class DateDisplayEn(DateDisplay): elif mod == Date.MOD_SPAN: d1 = self.display_cal[cal](start) d2 = self.display_cal[cal](date.get_stop_date()) - return "%sfrom %s to %s%s" % (qual_str, d1, d2, self.calendar[cal]) + scal = self.format_extras(cal, newyear) + return "%sfrom %s to %s%s" % (qual_str, d1, d2, scal) elif mod == Date.MOD_RANGE: d1 = self.display_cal[cal](start) d2 = self.display_cal[cal](date.get_stop_date()) - return "%sbetween %s and %s%s" % (qual_str, d1, d2, - self.calendar[cal]) + scal = self.format_extras(cal, newyear) + return "%sbetween %s and %s%s" % (qual_str, d1, d2, scal) else: text = self.display_cal[date.get_calendar()](start) - return "%s%s%s%s" % (qual_str, self._mod_str[mod], - text, self.calendar[cal]) + scal = self.format_extras(cal, newyear) + return "%s%s%s%s" % (qual_str, self._mod_str[mod], text, scal) diff --git a/src/DateHandler/_DateParser.py b/src/DateHandler/_DateParser.py index 04970ce34..99f32906d 100644 --- a/src/DateHandler/_DateParser.py +++ b/src/DateHandler/_DateParser.py @@ -173,6 +173,12 @@ class DateParser: 'persian' : Date.CAL_PERSIAN, 'p' : Date.CAL_PERSIAN, } + + newyear_to_int = { + "mar1": Date.NEWYEAR_MAR1, + "mar25": Date.NEWYEAR_MAR25, + "sep1" : Date.NEWYEAR_SEP1, + } quality_to_int = { 'estimated' : Date.QUAL_ESTIMATED, @@ -240,6 +246,7 @@ class DateParser: self._pmon_str = self.re_longest_first(self.persian_to_int.keys()) self._imon_str = self.re_longest_first(self.islamic_to_int.keys()) self._cal_str = self.re_longest_first(self.calendar_to_int.keys()) + self._ny_str = self.re_longest_first(self.newyear_to_int.keys()) # bce, calendar type and quality may be either at the end or at # the beginning of the given date string, therefore they will @@ -248,6 +255,11 @@ class DateParser: self._cal = re.compile("(.*)\s+\(%s\)( ?.*)" % self._cal_str, re.IGNORECASE) + self._calny = re.compile("(.*)\s+\(%s,%s\)( ?.*)" % (self._cal_str, + self._ny_str), + re.IGNORECASE) + self._ny = re.compile("(.*)\s+\(%s\)( ?.*)" % self._ny_str, + re.IGNORECASE) self._qual = re.compile("(.* ?)%s\s+(.+)" % self._qual_str, re.IGNORECASE) @@ -448,6 +460,31 @@ class DateParser: text = match.group(1) + match.group(3) return (text, cal) + def match_calendar_newyear(self, text, cal, newyear): + """ + Try parsing calendar and newyear code. + + Return newyear index and the text with calendar removed. + """ + match = self._calny.match(text) + if match: + cal = self.calendar_to_int[match.group(2).lower()] + newyear = self.newyear_to_int[match.group(3).lower()] + text = match.group(1) + match.group(4) + return (text, cal, newyear) + + def match_newyear(self, text, newyear): + """ + Try parsing calendar and newyear code. + + Return newyear index and the text with calendar removed. + """ + match = self._ny.match(text) + if match: + newyear = self.newyear_to_int[match.group(2).lower()] + text = match.group(1) + match.group(3) + return (text, newyear) + def match_quality(self, text, qual): """ Try matching quality. @@ -460,7 +497,7 @@ class DateParser: text = match.group(1) + match.group(3) return (text, qual) - def match_span(self, text, cal, qual, date): + def match_span(self, text, cal, ny, qual, date): """ Try matching span date. @@ -479,11 +516,11 @@ class DateParser: if bc2: stop = self.invert_year(stop) - date.set(qual, Date.MOD_SPAN, cal, start + stop) + date.set(qual, Date.MOD_SPAN, cal, start + stop, newyear=ny) return 1 return 0 - def match_range(self, text, cal, qual, date): + def match_range(self, text, cal, ny, qual, date): """ Try matching range date. @@ -502,7 +539,7 @@ class DateParser: if bc2: stop = self.invert_year(stop) - date.set(qual, Date.MOD_RANGE, cal, start + stop) + date.set(qual, Date.MOD_RANGE, cal, start + stop, newyear=ny) return 1 return 0 @@ -523,7 +560,7 @@ class DateParser: bc = True return (text, bc) - def match_modifier(self, text, cal, qual, bc, date): + def match_modifier(self, text, cal, ny, qual, bc, date): """ Try matching date with modifier. @@ -539,9 +576,9 @@ class DateParser: date.set_modifier(Date.MOD_TEXTONLY) date.set_text_value(text) elif bc: - date.set(qual, mod, cal, self.invert_year(start)) + date.set(qual, mod, cal, self.invert_year(start), newyear=ny) else: - date.set(qual, mod, cal, start) + date.set(qual, mod, cal, start, newyear=ny) return True # modifiers after the date if self.modifier_after_to_int: @@ -552,9 +589,9 @@ class DateParser: mod = self.modifier_after_to_int.get(grps[1].lower(), Date.MOD_NONE) if bc: - date.set(qual, mod, cal, self.invert_year(start)) + date.set(qual, mod, cal, self.invert_year(start), newyear=ny) else: - date.set(qual, mod, cal, start) + date.set(qual, mod, cal, start, newyear=ny) return True match = self._abt2.match(text) if match: @@ -562,9 +599,9 @@ class DateParser: start = self._parse_subdate(grps[0]) mod = Date.MOD_ABOUT if bc: - date.set(qual, mod, cal, self.invert_year(start)) + date.set(qual, mod, cal, self.invert_year(start), newyear=ny) else: - date.set(qual, mod, cal, start) + date.set(qual, mod, cal, start, newyear=ny) return True return False @@ -576,17 +613,20 @@ class DateParser: date.set_text_value(text) qual = Date.QUAL_NONE cal = Date.CAL_GREGORIAN + newyear = Date.NEWYEAR_JAN1 + (text, cal, newyear) = self.match_calendar_newyear(text, cal, newyear) (text, cal) = self.match_calendar(text, cal) + (text, newyear) = self.match_newyear(text, newyear) (text, qual) = self.match_quality(text, qual) - if self.match_span(text, cal, qual, date): + if self.match_span(text, cal, newyear, qual, date): return - if self.match_range(text, cal, qual, date): + if self.match_range(text, cal, newyear, qual, date): return (text, bc) = self.match_bce(text) - if self.match_modifier(text, cal, qual, bc, date): + if self.match_modifier(text, cal, newyear, qual, bc, date): return try: @@ -599,13 +639,9 @@ class DateParser: return if bc: - date.set(qual, Date.MOD_NONE, cal, self.invert_year(subdate)) + date.set(qual, Date.MOD_NONE, cal, self.invert_year(subdate), newyear=newyear) else: - date.set(qual, Date.MOD_NONE, cal, subdate) - - if date.get_slash(): - date.set_calendar(Date.CAL_JULIAN) - date.recalc_sort_value() # needed after the calendar change + date.set(qual, Date.MOD_NONE, cal, subdate, newyear=newyear) def invert_year(self, subdate): return (subdate[0], subdate[1], -subdate[2], subdate[3]) diff --git a/src/gen/lib/date.py b/src/gen/lib/date.py index 16334c8a5..9d6fae82b 100644 --- a/src/gen/lib/date.py +++ b/src/gen/lib/date.py @@ -259,6 +259,9 @@ class Span: def is_valid(self): return self.valid + def tuple(self): + return self._diff(self.date1, self.date2) + def __getitem__(self, pos): # Depricated! return self._diff(self.date1, self.date2)[pos] @@ -664,6 +667,8 @@ class Date: if isinstance(source, tuple): if calendar is None: self.calendar = Date.CAL_GREGORIAN + elif isinstance(calendar, int): + self.calendar = calendar else: self.calendar = self.lookup_calendar(calendar) if modifier is None: @@ -1165,6 +1170,18 @@ class Date: """ return self._get_low_item(Date._POS_YR) + def get_new_year(self): + """ + Return the new year code associated with the date. + """ + return self.newyear + + def set_new_year(self, value): + """ + Set the new year code associated with the date. + """ + self.newyear = value + def set_yr_mon_day(self, year, month, day): """ Set the year, month, and day values. @@ -1335,7 +1352,8 @@ class Date: """ return self.text - def set(self, quality, modifier, calendar, value, text=None): + def set(self, quality, modifier, calendar, value, text=None, + newyear=0): """ Set the date to the specified value. @@ -1379,14 +1397,38 @@ class Date: self.modifier = modifier self.calendar = calendar self.dateval = value + self.set_new_year(newyear) year = max(value[Date._POS_YR], 1) month = max(value[Date._POS_MON], 1) day = max(value[Date._POS_DAY], 1) + if year == month == 0 and day == 0: self.sortval = 0 else: func = Date._calendar_convert[calendar] self.sortval = func(year, month, day) + + if self.get_slash() and self.get_calendar() != Date.CAL_JULIAN: + self.set_calendar(Date.CAL_JULIAN) + self.recalc_sort_value() + + ny = self.get_new_year() + if ny: # new year offset? + if ny == Date.NEWYEAR_MAR1: + split = (3, 1) + elif ny == Date.NEWYEAR_MAR25: + split = (3, 25) + elif ny == Date.NEWYEAR_SEP1: + split = (9, 1) + if (self.get_month(), self.get_day()) >= split: + d1 = Date(self.get_year(), 1, 1, calendar=self.calendar).sortval + d2 = Date(self.get_year(), split[0], split[1], calendar=self.calendar).sortval + self.sortval -= (d2 - d1) + else: + d1 = Date(self.get_year(), 12, 31, calendar=self.calendar).sortval + d2 = Date(self.get_year(), split[0], split[1], calendar=self.calendar).sortval + self.sortval += (d1 - d2) + 1 + if text: self.text = text @@ -1551,10 +1593,16 @@ class Date: def get_slash(self): """ - Return true if the date is a slash-date. + Return true if the date is a slash-date (dual dated). """ return self._get_low_item_valid(Date._POS_SL) + def set_slash(self, value): + """ + Set to 1 if the date is a slash-date (dual dated). + """ + self.dateval[Date._POS_SL] = value + def Today(): """ Returns a Date object set to the current date. diff --git a/src/glade/gramps.glade b/src/glade/gramps.glade index 382b57a76..5f4e8cba7 100644 --- a/src/glade/gramps.glade +++ b/src/glade/gramps.glade @@ -9354,6 +9354,108 @@ + + + True + False + 0 + + + + True + + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 15 + False + False + + + + + + True + Old Style/New Style + True + Dua_l dated + True + GTK_RELIEF_NORMAL + True + False + False + True + + + 0 + False + False + + + + + + True + Ne_w year begins: + True + False + GTK_JUSTIFY_RIGHT + False + False + 1 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + True + True + + + + + + True + January 1 +March 1 +March 25 +September 1 + + False + True + + + + 0 + True + True + + + + + 6 + True + True + + + True @@ -9813,8 +9915,8 @@ 6 - True - True + False + False @@ -9870,9 +9972,9 @@ - 6 - False - False + 0 + True + True