From 76ee061b2555d63df270886b38693eeb93d3c5be Mon Sep 17 00:00:00 2001 From: Steve Youngs Date: Mon, 11 May 2020 20:57:04 +0100 Subject: [PATCH 1/2] Teach DateParser about calendar quarter dates The calendar year can be divided into four quarters, often abbreviated as Q1, Q2, Q3, and Q4. Q1 is 1 January to 31 March, Q2 1 April to 30 June, Q3 is 1 July to 30 September and Q4 is 1 October to 31 December This commit teaches DateParser to parse quarter dates and represet them as a date range. An example quarter date is "Q2 2020" which is converted to "between 1 April 2020 and 30 June 2020". This is a one way conversion --- gramps/gen/datehandler/_dateparser.py | 31 +++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/gramps/gen/datehandler/_dateparser.py b/gramps/gen/datehandler/_dateparser.py index d123279d5..27265d23e 100644 --- a/gramps/gen/datehandler/_dateparser.py +++ b/gramps/gen/datehandler/_dateparser.py @@ -4,6 +4,7 @@ # # Copyright (C) 2004-2006 Donald N. Allingham # Copyright (C) 2017 Paul Franklin +# Copyright (c) 2020 Steve Youngs # # 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 @@ -445,6 +446,9 @@ class DateParser: self._range = re.compile( r"(bet|bet.|between)\s+(?P.+)\s+and\s+(?P.+)", re.IGNORECASE) + self._quarter = re.compile( + r"[qQ](?P[1-4])\s+(?P.+)", + re.IGNORECASE) self._modifier = re.compile(r'%s\s+(.*)' % self._mod_str, re.IGNORECASE) self._modifier_after = re.compile(r'(.*)\s+%s' % self._mod_after_str, @@ -836,6 +840,31 @@ class DateParser: return 1 return 0 + def match_quarter(self, text, cal, ny, qual, date): + """ + Try matching calendar quarter date. + + On success, set the date and return 1. On failure return 0. + """ + match = self._quarter.match(text) + if match: + quarter = self._get_int(match.group('quarter')) + + text_parser = self.parser[cal] + (text, bc) = self.match_bce(match.group('year')) + start = self._parse_subdate(text, text_parser, cal) + if (start == Date.EMPTY and text != "") or (start[0] != 0) or (start[1] != 0): # reject dates where the day or month have been set + return 0 + if bc: + start = self.invert_year(start) + + stop_month = quarter * 3 + stop_day = _max_days[stop_month - 1] # no need to worry about leap years since no quarter ends in February + + date.set(qual, Date.MOD_RANGE, cal, (1, stop_month - 2, start[2], start[3]) + (stop_day, stop_month, start[2], start[3]), newyear=ny) + return 1 + return 0 + def match_bce(self, text): """ Try matching BCE qualifier. @@ -923,6 +952,8 @@ class DateParser: return if self.match_range(text, cal, newyear, qual, date): return + if self.match_quarter(text, cal, newyear, qual, date): + return (text, bc) = self.match_bce(text) if self.match_modifier(text, cal, newyear, qual, bc, date): From 183280b35a96a210da9cfc83d6a51eb52e85be2e Mon Sep 17 00:00:00 2001 From: Nick Hall Date: Sat, 19 Feb 2022 21:06:57 +0000 Subject: [PATCH 2/2] Add unit tests for quarter dates --- .../gen/datehandler/test/dateparser_test.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/gramps/gen/datehandler/test/dateparser_test.py b/gramps/gen/datehandler/test/dateparser_test.py index 074fed89a..2b24ac1e2 100644 --- a/gramps/gen/datehandler/test/dateparser_test.py +++ b/gramps/gen/datehandler/test/dateparser_test.py @@ -77,6 +77,39 @@ class DateParserTest(unittest.TestCase): self.assert_map_key_val(self.parser.calendar_to_int, 'юлианский', Date.CAL_JULIAN) self.assert_map_key_val(self.parser.calendar_to_int, 'ю', Date.CAL_JULIAN) + def test_quarter_1(self): + date = self.parser.parse('q1 1900') + self.assertTrue(date.is_equal(self.parser.parse('Q1 1900'))) + self.assertEqual(date.get_ymd(), (1900, 1, 1)) + self.assertEqual(date.get_stop_ymd(), (1900, 3, 31)) + self.assertEqual(date.get_modifier(), Date.MOD_RANGE) + + def test_quarter_2(self): + date = self.parser.parse('q2 1900') + self.assertTrue(date.is_equal(self.parser.parse('Q2 1900'))) + self.assertEqual(date.get_ymd(), (1900, 4, 1)) + self.assertEqual(date.get_stop_ymd(), (1900, 6, 30)) + self.assertEqual(date.get_modifier(), Date.MOD_RANGE) + + def test_quarter_3(self): + date = self.parser.parse('q3 1900') + self.assertTrue(date.is_equal(self.parser.parse('Q3 1900'))) + self.assertEqual(date.get_ymd(), (1900, 7, 1)) + self.assertEqual(date.get_stop_ymd(), (1900, 9, 30)) + self.assertEqual(date.get_modifier(), Date.MOD_RANGE) + + def test_quarter_4(self): + date = self.parser.parse('q4 1900') + self.assertTrue(date.is_equal(self.parser.parse('Q4 1900'))) + self.assertEqual(date.get_ymd(), (1900, 10, 1)) + self.assertEqual(date.get_stop_ymd(), (1900, 12, 31)) + self.assertEqual(date.get_modifier(), Date.MOD_RANGE) + + def test_quarter_quality_calendar(self): + date = self.parser.parse('calc q1 1900 (julian)') + self.assertEqual(date.get_quality(), Date.QUAL_CALCULATED) + self.assertEqual(date.get_calendar(), Date.CAL_JULIAN) + class Test_generate_variants(unittest.TestCase): def setUp(self): from .. import _datestrings