diff --git a/gnssanalysis/filenames.py b/gnssanalysis/filenames.py index 8c7cf3f..01b5329 100644 --- a/gnssanalysis/filenames.py +++ b/gnssanalysis/filenames.py @@ -370,12 +370,20 @@ def generate_IGS_nominal_span(start_epoch: datetime.datetime, end_epoch: datetim return nominal_span_string(span) -def nominal_span_string(span_seconds: float) -> str: +def nominal_span_string(span_seconds: float, igs_week_as_days: bool = True) -> str: """Generate the 3 character LEN or SMP string for IGS filenames based on total span seconds :param float span_seconds: Number of seconds in span of interest + :param bool igs_week_as_days: Use IGS common convention of expressing 1 week as 07D rather than 01W (in contrast + to IGS long filename spec which says to use the largest unit, i.e. 01W). Default: True (07D) :return str: 3 character span string as per IGS standard """ + + if span_seconds is None or span_seconds == 0: + return "00U" + + if span_seconds < 0: + raise ValueError(f"Span seconds can't be negative: {span_seconds}") # A year is ambiguous, for our purposes we want 365 (not 365.25 or 366) days, as we # use it as a lower bound sec_in_year = 365 * gn_const.SEC_IN_DAY @@ -397,7 +405,13 @@ def nominal_span_string(span_seconds: float) -> str: num_weeks = int(span_seconds // gn_const.SEC_IN_WEEK) # IGS uses 07D to represent a week # TODO: Handle JPL - uses 01W for a week - if (span_seconds % gn_const.SEC_IN_WEEK) < gn_const.SEC_IN_DAY and num_weeks > 1: + # We stick to outputting in weeks if either: + # - there are >=14 days (>1 week with int division) or + # - there are >=7 days (>0 weeks with int division), but ONLY if we are NOT applying IGS (inconsistent) + # convention of outputting 1 week as 07D + if (span_seconds % gn_const.SEC_IN_WEEK) < gn_const.SEC_IN_DAY and ( + num_weeks > 1 or (num_weeks > 0 and igs_week_as_days == False) + ): unit = "W" span_unit_counts = num_weeks else: @@ -440,7 +454,7 @@ def convert_nominal_span( ) -> Union[datetime.timedelta, None]: """Effectively invert :func: `filenames.generate_nominal_span`, turn a span string into a timedelta - :param str nominal_span: Three-character span string in IGS format (e.g. 01D, 15M, 01L ?) + :param str nominal_span: Three-character span string in IGS format (e.g. 01D, 15M, 01L) :param Literal["none", "timedelta"] non_timed_span_output: when a non-timed span e.g. '00U' is encountered, return a zero-length timedelta (default), or return None. instead of raising / warning / returning zero-length timedelta. diff --git a/tests/test_filenames.py b/tests/test_filenames.py index b7c40b9..eeace0e 100644 --- a/tests/test_filenames.py +++ b/tests/test_filenames.py @@ -266,3 +266,44 @@ def test_convert_nominal_span(self): with self.assertRaises(ValueError): filenames.convert_nominal_span("005M") + + def test_nominal_span_string(self): + + # Standard conversions + + # 15 sec + self.assertEqual(filenames.nominal_span_string(15), "15S") + + # 5 mins + self.assertEqual(filenames.nominal_span_string(300), "05M") + self.assertEqual(filenames.nominal_span_string(300.0), "05M") + + # 6 hours + self.assertEqual(filenames.nominal_span_string(60 * 60 * 6), "06H") + + # One day + self.assertEqual(filenames.nominal_span_string(60 * 60 * 24), "01D") + + # 36 hours must be expressed as hours not days, to preserve precision + self.assertEqual(filenames.nominal_span_string(60 * 60 * 36), "36H") + + # 2 days + self.assertEqual(filenames.nominal_span_string(60 * 60 * 24 * 2), "02D") + + # 1 week (we follow IGS' lead in expressing this as 07D, despite the IGS long filename spec saying it + # should be 01W) + # By default, IGS style weeks: 07D + self.assertEqual(filenames.nominal_span_string(60 * 60 * 24 * 7), "07D") + + # Optionally, format compliant weeks: 01W + self.assertEqual(filenames.nominal_span_string(60 * 60 * 24 * 7, igs_week_as_days=False), "01W") + + # 1 lunar cycle (28 days) # TODO note: currently unsupported. Will come out as '04W' + self.assertEqual(filenames.nominal_span_string(60 * 60 * 24 * 28), "04W") + + # Ensure smaller unit is used to preserve presision when needed. + self.assertEqual(filenames.nominal_span_string(65), "65S") + + # Up till the point it would be too big, then we 'up-shift' unit, and round up to the nearest value + # TODO this isn't currently supported + # self.assertEqual(filenames.nominal_span_string(110), "02M")