github.com/go-chrono/chrono@v0.0.0-20240102183611-532f0d0d7c34/period.go (about) 1 package chrono 2 3 import ( 4 "fmt" 5 "math" 6 "strconv" 7 "strings" 8 ) 9 10 // Period represents an amount of time in years, months, weeks and days. 11 // A period is not a measurable quantity since the lengths of these components is ambiguous. 12 type Period struct { 13 Years float32 14 Months float32 15 Weeks float32 16 Days float32 17 } 18 19 // Equal reports whether p and p2 represent the same period of time. 20 func (p Period) Equal(p2 Period) bool { 21 return p2.Years == p.Years && p2.Months == p.Months && p2.Weeks == p.Weeks && p2.Days == p.Days 22 } 23 24 // String returns a string formatted according to ISO 8601. 25 // It is equivalent to calling [Format] with no arguments. 26 func (p Period) String() string { 27 return p.Format() 28 } 29 30 // Format the duration according to ISO 8601. 31 // The output consists of only the period component - the time component is never included. 32 func (p Period) Format() string { 33 if p.Years == 0 && p.Months == 0 && p.Weeks == 0 && p.Days == 0 { 34 return "P0D" 35 } 36 37 out := "P" 38 if p.Years != 0 { 39 out += strconv.FormatFloat(math.Abs(float64(p.Years)), 'f', -1, 32) + "Y" 40 } 41 42 if p.Months != 0 { 43 out += strconv.FormatFloat(math.Abs(float64(p.Months)), 'f', -1, 32) + "M" 44 } 45 46 if p.Weeks != 0 { 47 out += strconv.FormatFloat(math.Abs(float64(p.Weeks)), 'f', -1, 32) + "W" 48 } 49 50 if p.Days != 0 { 51 out += strconv.FormatFloat(math.Abs(float64(p.Days)), 'f', -1, 32) + "D" 52 } 53 return out 54 } 55 56 // Parse the period portion of an ISO 8601 duration. 57 // This function supports the ISO 8601-2 extension, which allows weeks (W) to appear in combination 58 // with years, months, and days, such as P3W1D. Additionally, it allows a sign character to appear 59 // at the start of string, such as +P1M, or -P1M. 60 func (p *Period) Parse(s string) error { 61 years, months, weeks, days, _, _, _, err := parseDuration(s, true, false) 62 if err != nil { 63 return err 64 } 65 66 p.Years = years 67 p.Months = months 68 p.Weeks = weeks 69 p.Days = days 70 return nil 71 } 72 73 // FormatDuration formats a combined period and duration to a complete ISO 8601 duration. 74 func FormatDuration(p Period, d Duration, exclusive ...Designator) string { 75 out := p.Format() 76 77 t, neg := d.format(exclusive...) 78 out += t 79 80 if neg { 81 out = "-" + out 82 } 83 return out 84 } 85 86 // ParseDuration parses a complete ISO 8601 duration. 87 func ParseDuration(s string) (Period, Duration, error) { 88 years, months, weeks, days, secs, nsec, neg, err := parseDuration(s, true, true) 89 return Period{ 90 Years: years, 91 Months: months, 92 Weeks: weeks, 93 Days: days, 94 }, makeDuration(secs, nsec, neg), err 95 } 96 97 func parseDuration(s string, parsePeriod, parseTime bool) (years, months, weeks, days float32, secs int64, nsec uint32, neg bool, err error) { 98 if len(s) == 0 { 99 return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("empty string") 100 } 101 102 offset := 1 103 if s[0] == '+' { 104 offset++ 105 } else if s[0] == '-' { 106 neg = true 107 offset++ 108 } else if s[0] != 'P' { 109 return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("expecting 'P'") 110 } 111 112 var value int 113 var onTime bool 114 var haveUnit bool 115 116 for i := offset; i < len(s); i++ { 117 digit := (s[i] >= '0' && s[i] <= '9') || s[i] == '.' || s[i] == ',' 118 119 if value == 0 { 120 if digit { 121 value = i 122 } else if s[i] == 'T' { 123 if !onTime { 124 onTime = true 125 } else { 126 return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("unexpected '%c', expecting digit", s[i]) 127 } 128 } else { 129 return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("unexpected '%c', expecting digit or 'T'", s[i]) 130 } 131 } else { 132 if !onTime { 133 if !parsePeriod { 134 return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("cannot parse duration as Duration") 135 } else if digit { 136 continue 137 } 138 139 v, err := parseFloat(s[value:i], 32) 140 if err != nil { 141 return 0, 0, 0, 0, 0, 0, false, err 142 } 143 144 switch s[i] { 145 case 'Y': 146 years = float32(v) 147 case 'M': 148 months = float32(v) 149 case 'W': 150 weeks = float32(v) 151 case 'D': 152 days = float32(v) 153 default: 154 return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("unexpected '%c', expecting 'Y', 'M', 'W', or 'D'", s[i]) 155 } 156 157 value = 0 158 haveUnit = true 159 } else { 160 if !parseTime { 161 return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("cannot parse duration as Period") 162 } else if digit { 163 continue 164 } 165 166 v, err := parseFloat(s[value:i], 64) 167 if err != nil { 168 return 0, 0, 0, 0, 0, 0, false, err 169 } 170 171 var _secs float64 172 var _nsec uint32 173 switch s[i] { 174 case 'H': 175 _secs = math.Floor(v * 3600) 176 _nsec = uint32((v * 3.6e12) - (_secs * 1e9)) 177 case 'M': 178 _secs = math.Floor(v * 60) 179 _nsec = uint32((v * 6e10) - (_secs * 1e9)) 180 case 'S': 181 _secs = math.Floor(v) 182 _nsec = uint32((v * 1e9) - (_secs * 1e9)) 183 default: 184 return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("unexpected '%c', expecting 'H', 'M' or 'S'", s[i]) 185 } 186 187 if _secs < math.MinInt64 { 188 return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("seconds underflow") 189 } else if _secs > math.MaxInt64 { 190 return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("seconds overflow") 191 } 192 193 var under, over bool 194 if secs, under, over = addInt64(secs, int64(_secs)); under { 195 return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("seconds underflow") 196 } else if over { 197 return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("seconds overflow") 198 } 199 200 if secs, under, over = addInt64(secs, int64(_nsec/1e9)); under { 201 return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("seconds underflow") 202 } else if over { 203 return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("seconds overflow") 204 } 205 nsec = _nsec % 1e9 206 207 value = 0 208 haveUnit = true 209 } 210 } 211 } 212 213 if !haveUnit { 214 return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("expecting at least one unit") 215 } 216 return 217 } 218 219 func parseFloat(s string, bitSize int) (float64, error) { 220 s = strings.ReplaceAll(s, ",", ".") 221 return strconv.ParseFloat(s, bitSize) 222 }