LCOV - code coverage report
Current view: top level - src/formats - duration.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 100.0 % 36 36
Test Date: 2026-06-16 03:31:00 Functions: - 0 0
Legend: Lines: hit not hit

            Line data    Source code
       1              : // Copyright 2026 The Authors
       2              : //
       3              : // Licensed under the Apache License, Version 2.0 (the "License");
       4              : // you may not use this file except in compliance with the License.
       5              : // You may obtain a copy of the License at
       6              : //
       7              : //      https://www.apache.org/licenses/LICENSE-2.0
       8              : //
       9              : // Unless required by applicable law or agreed to in writing, software
      10              : // distributed under the License is distributed on an "AS IS" BASIS,
      11              : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12              : // See the License for the specific language governing permissions and
      13              : // limitations under the License.
      14              : 
      15              : /// Duration ala ISO 8601.
      16              : ///
      17              : /// This is a purposefully simple implementation and is not intended
      18              : /// to provide DateTime-style functionality. The main issues is that
      19              : /// having [months] and [years] makes the duration contextual and we
      20              : /// can't then boil it down to seconds (e.g. _how many seconds in a
      21              : /// month?_).
      22              : ///
      23              : /// From my reading of Appendix A of RFC 3339, negative values are
      24              : /// not allowed.
      25              : ///
      26              : /// See:
      27              : ///
      28              : ///  - [Appendix A of RFC 3339](https://www.rfc-editor.org/info/rfc3339)
      29              : ///  - [Wikipedia](https://en.wikipedia.org/wiki/ISO_8601#Durations)
      30              : class Iso8601Duration {
      31            3 :   static final RegExp iso8601Duration = RegExp(
      32              :     r'^P'
      33              :     r'((?<years>\d+)Y)?'
      34              :     r'((?<months>\d+)M)?'
      35              :     r'((?<days>\d+)D)?'
      36              :     r'(T'
      37              :     r'((?<hours>\d+)H)?'
      38              :     r'((?<minutes>\d+)M)?'
      39              :     r'((?<seconds>\d+)S)?)?$',
      40              :   );
      41              : 
      42              :   final int years;
      43              :   final int months;
      44              :   final int days;
      45              :   final int hours;
      46              :   final int minutes;
      47              :   final int seconds;
      48              : 
      49            1 :   Iso8601Duration._({
      50              :     this.years = 0,
      51              :     this.months = 0,
      52              :     this.days = 0,
      53              :     this.hours = 0,
      54              :     this.minutes = 0,
      55              :     this.seconds = 0,
      56              :   });
      57              : 
      58              :   /// Whether this [Iso8601Duration] properties are an exact match with [other].
      59              :   ///
      60              :   /// Nothing fancy is done here - no converting to seconds or normalising, it's
      61              :   /// just a comparison of properties.
      62            1 :   @override
      63              :   bool operator ==(Object other) {
      64            1 :     if (other is Iso8601Duration &&
      65            3 :         seconds == other.seconds &&
      66            3 :         minutes == other.minutes &&
      67            3 :         hours == other.hours &&
      68            3 :         days == other.days &&
      69            3 :         months == other.months &&
      70            3 :         years == other.years) {
      71              :       return true;
      72              :     }
      73              :     return false;
      74              :   }
      75              : 
      76            1 :   @override
      77              :   String toString() {
      78            7 :     return 'P${years}Y${months}M${days}DT${hours}H${minutes}M${seconds}S';
      79              :   }
      80              : 
      81              :   /// Returns a [Duration] representation of this [Iso8601Duration].
      82              :   ///
      83              :   /// Importantly, this returns `null` if the [Iso8601Duration] has the
      84              :   /// [years] and/or the [months] fields set to a value other than zero.
      85            1 :   Duration? get duration {
      86            4 :     if (years != 0 && months != 0) {
      87              :       return null;
      88              :     }
      89              : 
      90            1 :     return Duration(
      91            1 :       seconds: seconds,
      92            1 :       minutes: minutes,
      93            1 :       hours: hours,
      94            1 :       days: days,
      95              :     );
      96              :   }
      97              : 
      98              :   /// Parse an ISO 8601 duration [input] string into a [Iso8601Duration].
      99              :   ///
     100              :   /// [maxInputLength] is the maximum number of [input] characters
     101              :   ///
     102              :   /// Examples:
     103              :   ///
     104              :   ///   - `P1S` - One second
     105              :   ///   - `P1M` - One month
     106              :   ///   - `P1MT1M` - One month and one minute
     107              :   ///   - `P1Y2M3DT4H5M6S` - One year, two months, three days, four hours, five minutes, six seconds
     108              :   ///
     109              :   /// Returns (null, false) if the [input] is not a valid duration.
     110            1 :   static Iso8601Duration? tryParse(String input, {int maxInputLength = 24}) {
     111              :     // Guard the RegEx from dodgy strings
     112            2 :     if (input.substring(0, 1) != 'P' ||
     113            2 :         input.length > maxInputLength ||
     114            1 :         [
     115              :           'Y',
     116              :           'M',
     117              :           'D',
     118              :           'H',
     119              :           'S',
     120              :           'T',
     121            4 :         ].contains(input.substring(input.length - 2))) {
     122              :       return null;
     123              :     }
     124              : 
     125            2 :     var match = iso8601Duration.firstMatch(input);
     126              :     if (match == null) {
     127              :       return null;
     128              :     }
     129              : 
     130            2 :     int? seconds = int.tryParse(match.namedGroup('seconds') ?? '0');
     131            2 :     int? minutes = int.tryParse(match.namedGroup('minutes') ?? '0');
     132            2 :     int? hours = int.tryParse(match.namedGroup('hours') ?? '0');
     133            2 :     int? days = int.tryParse(match.namedGroup('days') ?? '0');
     134            2 :     int? months = int.tryParse(match.namedGroup('months') ?? '0');
     135            2 :     int? years = int.tryParse(match.namedGroup('years') ?? '0');
     136              : 
     137            3 :     if ([seconds, minutes, hours, days, months, years].any((e) => e == null)) {
     138              :       return null;
     139              :     }
     140              : 
     141            1 :     return Iso8601Duration._(
     142              :       years: years!,
     143              :       months: months!,
     144              :       days: days!,
     145              :       hours: hours!,
     146              :       minutes: minutes!,
     147              :       seconds: seconds!,
     148              :     );
     149              :   }
     150              : 
     151            1 :   @override
     152              :   int get hashCode =>
     153            8 :       Object.hashAll([years, months, days, hours, minutes, seconds]);
     154              : 
     155            2 :   static bool isValid(String value) => tryParse(value) != null;
     156              : }
        

Generated by: LCOV version 2.0-1