datetime handling for gleam

added more funcionalities

Changed files
+198 -40
src
+1 -1
gleam.toml
··· 1 1 name = "birl" 2 - version = "1.3.2" 2 + version = "1.4.0" 3 3 4 4 description = "Date / Time handling for Gleam" 5 5 gleam = ">= 0.32.0"
+163 -26
src/birl.gleam
··· 66 66 67 67 Time( 68 68 now, 69 - offset_in_minutes 70 - * 60_000_000, 69 + offset_in_minutes * 60_000_000, 71 70 option.map(timezone, fn(tz) { 72 71 case 73 72 zones.list ··· 82 81 ) 83 82 } 84 83 85 - /// Use this to get the current time in utc 84 + /// use this to get the current time in utc 86 85 pub fn utc_now() -> Time { 87 86 let now = ffi_now() 88 87 let monotonic_now = ffi_monotonic_now() 89 88 Time(now, 0, option.Some("Etc/UTC"), option.Some(monotonic_now)) 90 89 } 91 90 92 - /// Use this to get the current time with a given offset. 91 + /// use this to get the current time with a given offset. 93 92 /// 94 - /// Some examples of acceptable offsets: 93 + /// some examples of acceptable offsets: 95 94 /// 96 95 /// `"+330", "03:30", "-8:00","-7", "-0400", "03"` 97 96 pub fn now_with_offset(offset: String) -> Result(Time, Nil) { ··· 112 111 let monotonic_now = ffi_monotonic_now() 113 112 Time( 114 113 now, 115 - offset 116 - * 1_000_000, 114 + offset * 1_000_000, 117 115 option.Some(timezone), 118 116 option.Some(monotonic_now), 119 117 ) ··· 128 126 ffi_monotonic_now() 129 127 } 130 128 129 + /// returns a string which is the date part of an ISO8601 string along with the offset 130 + pub fn to_date_string(value: Time) -> String { 131 + let #(#(year, month, day), _, offset) = to_parts(value) 132 + 133 + int.to_string(year) 134 + <> "-" 135 + <> { 136 + month 137 + |> int.to_string 138 + |> string.pad_left(2, "0") 139 + } 140 + <> "-" 141 + <> { 142 + day 143 + |> int.to_string 144 + |> string.pad_left(2, "0") 145 + } 146 + <> offset 147 + } 148 + 149 + /// like `to_date_string` except it does not contain the offset 150 + pub fn to_naive_date_string(value: Time) -> String { 151 + let #(#(year, month, day), _, _) = to_parts(value) 152 + 153 + int.to_string(year) 154 + <> "-" 155 + <> { 156 + month 157 + |> int.to_string 158 + |> string.pad_left(2, "0") 159 + } 160 + <> "-" 161 + <> { 162 + day 163 + |> int.to_string 164 + |> string.pad_left(2, "0") 165 + } 166 + } 167 + 168 + /// returns a string which is the time part of an ISO8601 string along with the offset 169 + pub fn to_time_string(value: Time) -> String { 170 + let #(_, #(hour, minute, second, milli_second), offset) = to_parts(value) 171 + 172 + { 173 + hour 174 + |> int.to_string 175 + |> string.pad_left(2, "0") 176 + } 177 + <> ":" 178 + <> { 179 + minute 180 + |> int.to_string 181 + |> string.pad_left(2, "0") 182 + } 183 + <> ":" 184 + <> { 185 + second 186 + |> int.to_string 187 + |> string.pad_left(2, "0") 188 + } 189 + <> "." 190 + <> { 191 + milli_second 192 + |> int.to_string 193 + |> string.pad_left(3, "0") 194 + } 195 + <> offset 196 + } 197 + 198 + /// like `to_time_string` except it does not contain the offset 199 + pub fn to_naive_time_string(value: Time) -> String { 200 + let #(_, #(hour, minute, second, milli_second), _) = to_parts(value) 201 + 202 + { 203 + hour 204 + |> int.to_string 205 + |> string.pad_left(2, "0") 206 + } 207 + <> ":" 208 + <> { 209 + minute 210 + |> int.to_string 211 + |> string.pad_left(2, "0") 212 + } 213 + <> ":" 214 + <> { 215 + second 216 + |> int.to_string 217 + |> string.pad_left(2, "0") 218 + } 219 + <> "." 220 + <> { 221 + milli_second 222 + |> int.to_string 223 + |> string.pad_left(3, "0") 224 + } 225 + } 226 + 131 227 pub fn to_iso8601(value: Time) -> String { 132 228 let #(#(year, month, day), #(hour, minute, second, milli_second), offset) = 133 229 to_parts(value) ··· 172 268 <> offset 173 269 } 174 270 175 - /// If you need to parse an `ISO8601` string, this is probably what you're looking for. 271 + /// if you need to parse an `ISO8601` string, this is probably what you're looking for. 176 272 /// 177 - /// Given the huge surface area that `ISO8601` covers, it does not make sense for `birl` 273 + /// given the huge surface area that `ISO8601` covers, it does not make sense for `birl` 178 274 /// to support all of it in one function, so this function parses only strings for which both 179 275 /// day and time of day can be extracted or deduced. Some acceptable examples are given below: 180 276 /// ··· 274 370 } 275 371 } 276 372 277 - /// This function parses `ISO8601` strings in which no date is specified, which 373 + /// this function parses `ISO8601` strings in which no date is specified, which 278 374 /// means such inputs don't actually represent a particular moment in time. That's why 279 375 /// the result of this function is an instance of `TimeOfDay` along with the offset specificed 280 376 /// in the string. Some acceptable examples are given below: ··· 295 391 pub fn parse_time_of_day(value: String) -> Result(#(TimeOfDay, String), Nil) { 296 392 let assert Ok(offset_pattern) = regex.from_string("(.*)([+|\\-].*)") 297 393 298 - let value = case 394 + let time_string = case 299 395 [string.starts_with(value, "T"), string.starts_with(value, "t")] 300 396 { 301 397 [True, _] | [_, True] -> string.drop_left(value, 1) ··· 303 399 } 304 400 305 401 use #(time_string, offset_string) <- result.then(case 306 - string.ends_with(value, "Z") 307 - || string.ends_with(value, "z") 402 + string.ends_with(time_string, "Z") 403 + || string.ends_with(time_string, "z") 308 404 { 309 405 True -> Ok(#(string.drop_right(value, 1), "+00:00")) 310 406 False -> ··· 351 447 } 352 448 } 353 449 450 + /// accepts fromats similar to the ones listed for `parse_time_of_day` except that there shoundn't be any offset information 451 + pub fn parse_naive_time_of_day( 452 + value: String, 453 + ) -> Result(#(TimeOfDay, String), Nil) { 454 + let time_string = case 455 + [string.starts_with(value, "T"), string.starts_with(value, "t")] 456 + { 457 + [True, _] | [_, True] -> string.drop_left(value, 1) 458 + _ -> value 459 + } 460 + 461 + let time_string = string.replace(time_string, ":", "") 462 + 463 + use #(time_string, milli_seconds_result) <- result.then(case 464 + [string.split(time_string, "."), string.split(time_string, ",")] 465 + { 466 + [[_], [_]] -> { 467 + Ok(#(time_string, Ok(0))) 468 + } 469 + [[time_string, milli_seconds_string], [_]] 470 + | [[_], [time_string, milli_seconds_string]] -> { 471 + Ok(#( 472 + time_string, 473 + milli_seconds_string 474 + |> string.slice(0, 3) 475 + |> string.pad_right(3, "0") 476 + |> int.parse, 477 + )) 478 + } 479 + 480 + _ -> Error(Nil) 481 + }) 482 + 483 + case milli_seconds_result { 484 + Ok(milli_seconds) -> { 485 + use time_of_day <- result.then(parse_time_section(time_string)) 486 + let assert [hour, minute, second] = time_of_day 487 + 488 + Ok(#(TimeOfDay(hour, minute, second, milli_seconds), "Z")) 489 + } 490 + Error(Nil) -> Error(Nil) 491 + } 492 + } 493 + 354 494 /// the naive format is the same as ISO8601 except that it does not contain the offset 355 495 pub fn to_naive(value: Time) -> String { 356 496 let #(#(year, month, day), #(hour, minute, second, milli_second), _) = ··· 395 535 } 396 536 } 397 537 398 - /// Accepts fromats similar to the ones listed for `parse` except that there shoundn't be any offset information 538 + /// accepts fromats similar to the ones listed for `parse` except that there shoundn't be any offset information 399 539 pub fn from_naive(value: String) -> Result(Time, Nil) { 400 540 let value = string.trim(value) 401 541 ··· 813 953 case mt { 814 954 option.Some(mt) -> 815 955 Time( 816 - wall_time: wt 817 - + duration, 956 + wall_time: wt + duration, 818 957 offset: o, 819 958 timezone: timezone, 820 959 monotonic_time: option.Some(mt + duration), 821 960 ) 822 961 option.None -> 823 962 Time( 824 - wall_time: wt 825 - + duration, 963 + wall_time: wt + duration, 826 964 offset: o, 827 965 timezone: timezone, 828 966 monotonic_time: option.None, ··· 837 975 case mt { 838 976 option.Some(mt) -> 839 977 Time( 840 - wall_time: wt 841 - - duration, 978 + wall_time: wt - duration, 842 979 offset: o, 843 980 timezone: timezone, 844 981 monotonic_time: option.Some(mt - duration), 845 982 ) 846 983 option.None -> 847 984 Time( 848 - wall_time: wt 849 - - duration, 985 + wall_time: wt - duration, 850 986 offset: o, 851 987 timezone: timezone, 852 988 monotonic_time: option.None, ··· 980 1116 981 1117 /// use this to change the offset of a given time value. 982 1118 /// 983 - /// Some examples of acceptable offsets: 1119 + /// some examples of acceptable offsets: 984 1120 /// 985 1121 /// `"+330", "03:30", "-8:00","-7", "-0400", "03", "Z"` 986 1122 pub fn set_offset(value: Time, new_offset: String) -> Result(Time, Nil) { ··· 1025 1161 1026 1162 @target(erlang) 1027 1163 /// calculates erlang datetime using the offset in the DateTime value 1028 - pub fn to_erlang_datetime(value: Time) -> #(#(Int, Int, Int), #(Int, Int, Int)) { 1164 + pub fn to_erlang_datetime( 1165 + value: Time, 1166 + ) -> #(#(Int, Int, Int), #(Int, Int, Int)) { 1029 1167 let #(date, #(hour, minute, second, _), _) = to_parts(value) 1030 1168 #(date, #(hour, minute, second)) 1031 1169 } ··· 1057 1195 1058 1196 Time( 1059 1197 wall_time, 1060 - offset_in_minutes 1061 - * 60_000_000, 1198 + offset_in_minutes * 60_000_000, 1062 1199 option.map(timezone, fn(tz) { 1063 1200 case 1064 1201 zones.list
+34 -13
src/birl/duration.gleam
··· 28 28 Duration(a + b) 29 29 } 30 30 31 + pub fn subtract(a: Duration, b: Duration) -> Duration { 32 + let Duration(a) = a 33 + let Duration(b) = b 34 + Duration(a - b) 35 + } 36 + 31 37 pub fn seconds(value: Int) -> Duration { 32 38 Duration(value * second) 33 39 } ··· 56 62 Duration(value * year) 57 63 } 58 64 59 - /// Use this if you need short durations where a year just means 365 days and a month just means 30 days 65 + /// use this if you need short durations where a year just means 365 days and a month just means 30 days 60 66 pub fn new(values: List(#(Int, Unit))) -> Duration { 61 67 values 62 68 |> list.fold(0, fn(total, current) { ··· 75 81 |> Duration 76 82 } 77 83 78 - /// Use this if you need very long durations where small inaccuracies could lead to large errors 84 + /// use this if you need very long durations where small inaccuracies could lead to large errors 79 85 pub fn accurate_new(values: List(#(Int, Unit))) -> Duration { 80 86 values 81 87 |> list.fold(0, fn(total, current) { ··· 94 100 |> Duration 95 101 } 96 102 97 - /// Use this if you need short durations where a year just means 365 days and a month just means 30 days 103 + /// use this if you need short durations where a year just means 365 days and a month just means 30 days 98 104 pub fn decompose(duration: Duration) -> List(#(Int, Unit)) { 99 105 let Duration(value) = duration 100 106 let absolute_value = int.absolute_value(value) ··· 127 133 }) 128 134 } 129 135 130 - /// Use this if you need very long durations where small inaccuracies could lead to large errors 136 + /// use this if you need very long durations where small inaccuracies could lead to large errors 131 137 pub fn accurate_decompose(duration: Duration) -> List(#(Int, Unit)) { 132 138 let Duration(value) = duration 133 139 let absolute_value = int.absolute_value(value) ··· 160 166 }) 161 167 } 162 168 169 + /// approximates the duration by only the given unit 170 + /// 171 + /// if the duration is not an integer multiple of the unit, 172 + /// the remainder will be disgarded if it's less than two thirds of the unit, 173 + /// otherwise a single unit will be added to the multiplier 174 + /// 175 + /// - `duration.blur_to(duration.days(16), duration.Month)` -> 0 176 + /// - `duration.blur_to(duration.days(20), duration.Month)` -> 1 177 + pub fn blur_to(duration: Duration, unit: Unit) -> Int { 178 + let assert Ok(unit_value) = list.key_find(unit_values, unit) 179 + let Duration(value) = duration 180 + let #(unit_counts, remaining) = extract(value, unit_value) 181 + case remaining >= unit_value * 2 / 3 { 182 + True -> unit_counts + 1 183 + False -> unit_counts 184 + } 185 + } 186 + 163 187 /// approximates the duration by a value in a single unit 164 188 pub fn blur(duration: Duration) -> #(Int, Unit) { 165 - case 166 - duration 167 - |> decompose 168 - { 189 + case decompose(duration) { 169 190 [] -> #(0, MicroSecond) 170 191 decomposed -> 171 192 decomposed ··· 256 277 #(MilliSecond, milli_second_units), 257 278 ] 258 279 259 - /// You can use this function to create a new duration using expressions like: 280 + /// you can use this function to create a new duration using expressions like: 260 281 /// 261 282 /// "accurate: 1 Year - 2days + 152M -1h + 25 years + 25secs" 262 283 /// ··· 278 299 /// 279 300 /// MilliSecond: ms, Msec, mSecs, milliSecond, MilliSecond, ... 280 301 /// 281 - /// Numbers with no unit are considered as microseconds. 282 - /// Specifying `accurate:` is equivalent to using `accurate_new`. 302 + /// numbers with no unit are considered as microseconds. 303 + /// specifying `accurate:` is equivalent to using `accurate_new`. 283 304 pub fn parse(expression: String) -> Result(Duration, Nil) { 284 305 let assert Ok(re) = regex.from_string("([+|\\-])?\\s*(\\d+)\\s*(\\w+)?") 285 306 ··· 338 359 } 339 360 } 340 361 341 - fn extract(duration: Int, unit: Int) -> #(Int, Int) { 342 - #(duration / unit, duration % unit) 362 + fn extract(duration: Int, unit_value: Int) -> #(Int, Int) { 363 + #(duration / unit_value, duration % unit_value) 343 364 }