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 }