diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d76bdb8..b5947ea 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,9 @@ Here you can see the full list of changes between each aerofiles release. aerofiles vXXX, 2024-XX-YYYY ---------------------------- - igc/reader: fix for HFTZN to handle timezone that is not integer +- igc/reader: add "datetime" and "datetime_local" to fix_records to + deal with UTC and local time correctly. Please read "How + to read an IGC file" for a detailed explanation. aerofiles v1.3.1, 2024-08-12 ---------------------------- diff --git a/aerofiles/igc/reader.py b/aerofiles/igc/reader.py index e4969d7..83dd417 100644 --- a/aerofiles/igc/reader.py +++ b/aerofiles/igc/reader.py @@ -1,5 +1,7 @@ import datetime +from aerofiles.util.timezone import TimeZoneFix + class Reader: """ @@ -57,6 +59,23 @@ def read(self, file_obj): fix_records[0].append(MissingExtensionsError) fix_record = LowLevelReader.process_B_record(line, fix_record_extensions[1]) + + # To create "datetime" we need a date. Take it from header or previous fix: + if len(fix_records[1]) == 0: + date = header[1]["utc_date"] + else: + previous_fix = fix_records[1][-1] + date = previous_fix["datetime"].date() + time = previous_fix["datetime"].time() + # If time of next fix is _before_ last fix, we are now on next day + if fix_record["time"] < time: + date = date + datetime.timedelta(days=1) + + fix_record["datetime"] = datetime.datetime.combine(date, fix_record["time"]).replace(tzinfo=TimeZoneFix(0)) + if "time_zone_offset" in header[1]: + timezone = TimeZoneFix(header[1]["time_zone_offset"]) + fix_record["datetime_local"] = fix_record["datetime"].astimezone(timezone) + fix_records[1].append(fix_record) elif record_type == 'C': task_item = line @@ -653,16 +672,7 @@ def decode_date(date_str): elif date_str == '000000': return None - dd = int(date_str[0:2]) - mm = int(date_str[2:4]) - yy = int(date_str[4:6]) - - current_year_yyyy = datetime.date.today().year - current_year_yy = current_year_yyyy % 100 - current_century = current_year_yyyy - current_year_yy - yyyy = current_century + yy if yy <= current_year_yy else current_century - 100 + yy - - return datetime.date(yyyy, mm, dd) + return datetime.datetime.strptime(date_str, "%d%m%y").date() @staticmethod def decode_time(time_str): @@ -670,11 +680,7 @@ def decode_time(time_str): if len(time_str) != 6: raise ValueError('Time string does not have correct size') - h = int(time_str[0:2]) - m = int(time_str[2:4]) - s = int(time_str[4:6]) - - return datetime.time(h, m, s) + return datetime.datetime.strptime(time_str, "%H%M%S").time() @staticmethod def decode_extension_record(line): diff --git a/aerofiles/igc/writer.py b/aerofiles/igc/writer.py index 80736de..37f74c1 100644 --- a/aerofiles/igc/writer.py +++ b/aerofiles/igc/writer.py @@ -1,6 +1,7 @@ import datetime from aerofiles.igc import patterns +from aerofiles.util.timezone import TimeZoneFix class Writer: @@ -513,7 +514,7 @@ def write_task_metadata( """ if declaration_datetime is None: - declaration_datetime = datetime.datetime.utcnow() + declaration_datetime = datetime.datetime.now(TimeZoneFix(0)) if isinstance(declaration_datetime, datetime.datetime): declaration_datetime = ( @@ -687,7 +688,7 @@ def write_fix(self, time=None, latitude=None, longitude=None, valid=False, """ if time is None: - time = datetime.datetime.utcnow() + time = datetime.datetime.now(TimeZoneFix(0)) record = self.format_time(time) record += self.format_latitude(latitude) @@ -754,7 +755,7 @@ def write_event(self, *args): time = None if time is None: - time = datetime.datetime.utcnow() + time = datetime.datetime.now(TimeZoneFix(0)) if not patterns.THREE_LETTER_CODE.match(code): raise ValueError('Invalid event code') @@ -790,7 +791,7 @@ def write_satellites(self, *args): time, satellites = args if time is None: - time = datetime.datetime.utcnow() + time = datetime.datetime.now(TimeZoneFix(0)) record = self.format_time(time) diff --git a/aerofiles/util/timezone.py b/aerofiles/util/timezone.py new file mode 100644 index 0000000..38d0dcf --- /dev/null +++ b/aerofiles/util/timezone.py @@ -0,0 +1,16 @@ +import datetime + + +class TimeZoneFix(datetime.tzinfo): + + def __init__(self, fix): + self.fix = fix + + def utcoffset(self, dt): + return datetime.timedelta(hours=self.fix) + + def dst(self, dt): + return datetime.timedelta(0) + + def tzname(self, dt): + return str(self.fix) diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index 478f4b1..9a80c0a 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -7,6 +7,7 @@ This part of the documentation explains typical use cases with `aerofiles`. :maxdepth: 2 installation + guide/igc-reading guide/igc-writing API Reference diff --git a/tests/igc/data/example.igc b/tests/igc/data/example.igc index 6ff3764..e402687 100644 --- a/tests/igc/data/example.igc +++ b/tests/igc/data/example.igc @@ -6,6 +6,7 @@ HFCM2CREW2: Smith-Barry John A HFGTYGLIDERTYPE: Schleicher ASH-25 HFGIDGLIDERID: ABCD-1234 HFDTM100GPSDATUM: WGS-1984 +HFTZNTIMEZONE:+3.00 HFRFWFIRMWAREVERSION:6.4 HFRHWHARDWAREVERSION:3.0 HFFTYFRTYPE: Manufacturer, Model @@ -43,7 +44,7 @@ E160305PEV B1603055107180N00149185WA002910043521008015 B1603105107212N00149174WA002930043519608024 K16024800090 -B1602485107220N00149150WA004940043619008018 +B2202485107220N00149150WA004940043619008018 B1602525107330N00149127WA004960043919508015 LXXXRURITANIAN STANDARD NATIONALS DAY 1 LXXXFLIGHT TIME: 4:14:25, TASK SPEED:58.48KTS diff --git a/tests/igc/test_reader.py b/tests/igc/test_reader.py index ec13769..8876ad4 100644 --- a/tests/igc/test_reader.py +++ b/tests/igc/test_reader.py @@ -632,7 +632,15 @@ def test_highlevel_reader(): 'manufacturer': 'XXX' } - assert len(result['fix_records'][1]) == 9 + fixes = result['fix_records'][1] + assert len(fixes) == 9 + + assert fixes[0]["datetime"] == datetime.datetime(2001, 7, 16, 16, 2, 40, tzinfo=datetime.timezone(datetime.timedelta(0))) + # check that timezone is +3 + assert fixes[0]["datetime_local"].time() == datetime.time(19, 2, 40) + + # Check, that we have the next day, because the time is lower than the previous fix + assert fixes[8]["datetime"].date() == datetime.date(2001, 7, 17) assert result['task'][1]['declaration_date'] == datetime.date(2001, 7, 15) assert result['task'][1]['declaration_time'] == datetime.time(21, 38, 41) @@ -674,6 +682,7 @@ def test_highlevel_reader(): 'pressure_sensor_max_alt': { 'unit': 'm', 'value': 11000}, + 'time_zone_offset': 3, 'utc_date': datetime.date(2001, 7, 16) }