github.com/go-chrono/chrono@v0.0.0-20240102183611-532f0d0d7c34/interval.go (about)

     1  package chrono
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  	"strings"
     7  )
     8  
     9  // Interval represents the intervening time between two time points.
    10  type Interval struct {
    11  	s *OffsetDateTime
    12  	e *OffsetDateTime
    13  	d *periodDuration
    14  	r int
    15  }
    16  
    17  type periodDuration struct {
    18  	Period
    19  	Duration
    20  }
    21  
    22  // IntervalOfStartEnd creates an [Interval] from the provided start and end time points.
    23  func IntervalOfStartEnd(start, end OffsetDateTime, repetitions int) Interval {
    24  	return Interval{s: &start, e: &end, r: repetitions}
    25  }
    26  
    27  // IntervalOfStartDuration creates an [Interval] from the provided start time point and duration.
    28  func IntervalOfStartDuration(start OffsetDateTime, period Period, duration Duration, repetitions int) Interval {
    29  	return Interval{s: &start, d: &periodDuration{Period: period, Duration: duration}, r: repetitions}
    30  }
    31  
    32  // IntervalOfDurationEnd creates an [Interval] from the provided duration and end time point.
    33  func IntervalOfDurationEnd(period Period, duration Duration, end OffsetDateTime, repetitions int) Interval {
    34  	return Interval{e: &end, d: &periodDuration{Period: period, Duration: duration}, r: repetitions}
    35  }
    36  
    37  // ParseInterval parses an ISO 8601 time interval, or a repeating time interval.
    38  // Time intervals can be expressed in the following forms:
    39  //   - <start>/<end>
    40  //   - <start>/<duration>
    41  //   - <duration>/<end>
    42  //   - <duration>
    43  //
    44  // where <start> and <end> is any string that can be parsed by [Parse],
    45  // and <duration> is any string that can be parsed by [ParseDuration].
    46  //
    47  // Repeating time intervals are expressed as such:
    48  //   - Rn/<interval>
    49  //   - R/<interval>
    50  //
    51  // where <interval> is one the forms in the first list, R is the character itself,
    52  // and n is a signed integer value of the number of repetitions. Additionally, the following values have special meanings:
    53  //   - 0 is no repetitions, equivalent to not including the repeat expression at all (i.e. using the forms in the first list as-is);
    54  //   - <=-1 is unbounded number of repetitions, equivalent to not specifying a value at all (i.e. 'R-1' is the same as 'R').
    55  //
    56  // Additionally, '--' can be used as the separator, instead of the default '/' character.
    57  func ParseInterval(s string) (Interval, error) {
    58  	start, end, pd, r, err := parseInterval(s)
    59  	return Interval{
    60  		s: start,
    61  		e: end,
    62  		d: pd,
    63  		r: r,
    64  	}, err
    65  }
    66  
    67  // String returns the formatted Interval that can be parsed by i.Parse().
    68  func (i Interval) String() string {
    69  	return i.string("/")
    70  }
    71  
    72  func (i Interval) string(sep string) string {
    73  	var out string
    74  	r := i.Repetitions()
    75  	switch r {
    76  	case 0:
    77  		// Omit R.
    78  	case -1:
    79  		out = "R" + sep
    80  	default:
    81  		out = "R" + strconv.Itoa(r) + sep
    82  	}
    83  
    84  	switch {
    85  	case i.s != nil && i.e != nil:
    86  		return out + i.s.Format(ISO8601) + sep + i.e.Format(ISO8601)
    87  	case i.s != nil && i.d != nil:
    88  		return out + i.s.Format(ISO8601) + sep + FormatDuration(i.d.Period, i.d.Duration)
    89  	case i.d != nil && i.e != nil:
    90  		return out + FormatDuration(i.d.Period, i.d.Duration) + sep + i.e.Format(ISO8601)
    91  	case i.d != nil:
    92  		return out + FormatDuration(i.d.Period, i.d.Duration)
    93  	default:
    94  		return out
    95  	}
    96  }
    97  
    98  // Start returns the start time point if present, or a calculated time point if possible
    99  // by subtracting i.Duration() from i.End().
   100  // If neither are possible (i.e. only a duration is present),
   101  // [ErrUnsupportedRepresentation] is returned instead.
   102  func (i Interval) Start() (OffsetDateTime, error) {
   103  	switch {
   104  	case i.s != nil:
   105  		return *i.s, nil
   106  	case i.e != nil:
   107  		d, err := i.d.mul(-1)
   108  		if err != nil {
   109  			return OffsetDateTime{}, err
   110  		}
   111  		return i.e.AddDate(-int(i.d.Years), -int(i.d.Months), -int(i.d.Days)).Add(d), nil
   112  	default:
   113  		return OffsetDateTime{}, ErrUnsupportedRepresentation
   114  	}
   115  }
   116  
   117  // End returns the end time point if present, or a calculated time point if possible
   118  // by adding i.Duration() to i.Start().
   119  // If neither are possible, (i.e. only a duration is present),
   120  // then [ErrUnsupportedRepresentation] is returned instead.
   121  func (i Interval) End() (OffsetDateTime, error) {
   122  	switch {
   123  	case i.e != nil:
   124  		return *i.e, nil
   125  	case i.s != nil:
   126  		return i.s.AddDate(int(i.d.Years), int(i.d.Months), int(i.d.Days)).Add(i.d.Duration), nil
   127  	default:
   128  		return OffsetDateTime{}, ErrUnsupportedRepresentation
   129  	}
   130  }
   131  
   132  // Duration returns the [Period] and [Duration] if present, or a calculated [Duration]
   133  // if possible by substracting i.Start() from i.End(). Note that the latter case,
   134  // the [Period] returned will always be the zero value.
   135  func (i Interval) Duration() (Period, Duration, error) {
   136  	switch {
   137  	case i.d != nil:
   138  		return i.d.Period, i.d.Duration, nil
   139  	case i.s != nil && i.e != nil:
   140  		return Period{}, i.e.Sub(*i.s), nil
   141  	default:
   142  		return Period{}, Duration{}, ErrUnsupportedRepresentation
   143  	}
   144  }
   145  
   146  // Repetitions returns the number of repetitions of a repeating interval.
   147  // Any negative number, meaning an unbounded number of repitions, is normalized to -1.
   148  func (i Interval) Repetitions() int {
   149  	if i.r <= -1 {
   150  		return -1
   151  	}
   152  	return i.r
   153  }
   154  
   155  func cutAB(s, sepA, sepB string) (before, after string, found int) {
   156  	if i := strings.Index(s, sepA); i >= 0 {
   157  		return s[:i], s[i+len(sepA):], 1
   158  	} else if i := strings.Index(s, sepB); i >= 0 {
   159  		return s[:i], s[i+len(sepB):], -1
   160  	}
   161  	return s, "", 0
   162  }
   163  
   164  func parseInterval(s string) (start, end *OffsetDateTime, pd *periodDuration, repeat int, err error) {
   165  	if len(s) == 0 {
   166  		return nil, nil, nil, 0, fmt.Errorf("empty string")
   167  	}
   168  
   169  	var sep int
   170  
   171  	if s[0] == 'R' {
   172  		s = s[1:]
   173  
   174  		var r string
   175  		if r, s, sep = cutAB(s, "/", "--"); sep == 0 {
   176  			return nil, nil, nil, 0, fmt.Errorf("parsing interval: missing separator")
   177  		}
   178  
   179  		if len(r) == 0 {
   180  			repeat = -1
   181  		} else if repeat, err = strconv.Atoi(r); err != nil {
   182  			return nil, nil, nil, 0, fmt.Errorf("parsing interval: invalid repeat")
   183  		}
   184  	}
   185  
   186  	s1, s2, found := cutAB(s, "/", "--")
   187  	if found != 0 && sep != 0 && found != sep {
   188  		return nil, nil, nil, 0, fmt.Errorf("inconsistent separators")
   189  	}
   190  
   191  	if s1[0] >= '0' && s1[0] <= '9' { // <start>/<end> or <start>/<duation>
   192  		if s2 == "" { // <start> is invalid
   193  			return nil, nil, nil, 0, fmt.Errorf("invalid interval")
   194  		}
   195  
   196  		if start, err = parseOffsetDateTime(s1); err != nil {
   197  			return nil, nil, nil, 0, err
   198  		}
   199  	} else { // <duration>/<end> or <duration>
   200  		p, d, err := ParseDuration(s1)
   201  		if err != nil {
   202  			return nil, nil, nil, 0, err
   203  		}
   204  		pd = &periodDuration{p, d}
   205  	}
   206  
   207  	if s2 != "" && s2[0] >= '0' && s2[0] <= '9' { // <start>/<end> or <duration>/<end>
   208  		if end, err = parseOffsetDateTime(s2); err != nil {
   209  			return nil, nil, nil, 0, err
   210  		}
   211  	} else if s2 != "" { // <start>/<duation>
   212  		p, d, err := ParseDuration(s2)
   213  		if err != nil {
   214  			return nil, nil, nil, 0, err
   215  		}
   216  		pd = &periodDuration{p, d}
   217  	}
   218  
   219  	return start, end, pd, repeat, nil
   220  }
   221  
   222  func parseOffsetDateTime(value string) (*OffsetDateTime, error) {
   223  	var date, time, offset int64
   224  	if err := parseDateAndTime(ISO8601, value, &date, &time, &offset); err != nil {
   225  		return nil, err
   226  	}
   227  
   228  	return &OffsetDateTime{
   229  		v: makeDateTime(date, time),
   230  		o: offset,
   231  	}, nil
   232  }