Skip to content

Commit

Permalink
std/time: add text encoding and decoding support for Time
Browse files Browse the repository at this point in the history
  • Loading branch information
mertcandav committed Nov 28, 2024
1 parent cb2eae9 commit 5122ce6
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 8 deletions.
10 changes: 5 additions & 5 deletions std/time/format.jule
Original file line number Diff line number Diff line change
Expand Up @@ -1124,12 +1124,12 @@ fn commaOrPeriod(b: byte): bool {
ret b == '.' || b == ','
}

fn parseNanoseconds[bytes: []byte | str](mut value: bytes, mut nbytes: int)!: (ns: int) {
fn parseNanoseconds[bytes: []byte | str](value: bytes, mut nbytes: int)!: (ns: int) {
if !commaOrPeriod(value[0]) {
error(ParseError.BadField)
}
if nbytes > 10 {
value = value[:10]
unsafe { *(&value) = (*(&value))[:10] } // Break immutability for slicing.
nbytes = 10
}

Expand Down Expand Up @@ -1286,15 +1286,15 @@ fn leadingInt[bytes: []byte | str](s: bytes)!: (x: u64, rem: bytes) {
error(ParseError.LeadingInt)
}
}
ret x, s[i:]
ret x, unsafe { (*(&s))[i:] } // Break immutability for return.
}

// Duplicates functionality in strconv, but avoids dependency.
fn atoi[bytes: []byte | str](mut s: bytes)!: (x: int) {
fn atoi[bytes: []byte | str](s: bytes)!: (x: int) {
mut neg := false
if len(s) > 0 && (s[0] == '-' || s[0] == '+') {
neg = s[0] == '-'
s = s[1:]
unsafe { *(&s) = (*(&s))[1:] } // Break immutability for slicing.
}
q, rem := leadingInt(s) else { error(error) }
x = int(q)
Expand Down
59 changes: 56 additions & 3 deletions std/time/format_rfc3339.jule
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
// Use of this source code is governed by a BSD 3-Clause
// license that can be found in the LICENSE file.

// RFC 3339 is the most commonly used format.
// It is implicitly used by the Time.(Encode|Decode)(Text) methods.

use "std/unsafe"

fn appendFormatRFC3339(&t: Time, mut b: []byte, nanos: bool): []byte {
_, offset, abs := t.locabs()

Expand Down Expand Up @@ -45,7 +50,26 @@ fn appendFormatRFC3339(&t: Time, mut b: []byte, nanos: bool): []byte {
ret b
}

fn parseRFC3339[bytes: []byte | str](mut s: bytes, mut local: &Location): (Time, bool) {
fn appendStrictRFC3339(&t: Time, mut b: []byte): ([]byte, ok: bool) {
n0 := len(b)
b = appendFormatRFC3339(t, b, true)

// Not all valid Jule timestamps can be serialized as valid RFC 3339.
// Explicitly check for these edge cases.
num2 := fn(b: []byte): byte { ret 10*(b[0]-'0') + (b[1] - '0') }
match {
| b[n0+len("9999")] != '-': // year must be exactly 4 digits wide
ret b, false
| b[len(b)-1] != 'Z':
c := b[len(b)-len("Z07:00")]
if ('0' <= c && c <= '9') || num2(b[len(b)-len("07:00"):]) >= 24 {
ret b, false
}
}
ret b, true
}

fn parseRFC3339[bytes: []byte | str](s: bytes, mut local: &Location): (Time, bool) {
// parseUint parses s as an unsigned decimal integer and
// verifies that it is within some range.
// If it is invalid or out-of-range,
Expand Down Expand Up @@ -79,7 +103,7 @@ fn parseRFC3339[bytes: []byte | str](mut s: bytes, mut local: &Location): (Time,
if !ok || !(s[4] == '-' && s[7] == '-' && s[10] == 'T' && s[13] == ':' && s[16] == ':') {
ret Time{}, false
}
s = s[19:]
unsafe { *(&s) = (*(&s))[19:] } // Break immutability for slicing.

// Parse the fractional second.
mut nsec := 0
Expand All @@ -88,7 +112,7 @@ fn parseRFC3339[bytes: []byte | str](mut s: bytes, mut local: &Location): (Time,
for n < len(s) && isDigit(s, n); n++ {
}
nsec = parseNanoseconds(s, n) else { use 0 }
s = s[n:]
unsafe { *(&s) = (*(&s))[n:] } // Break immutability for slicing.
}

// Parse the time zone.
Expand Down Expand Up @@ -118,4 +142,33 @@ fn parseRFC3339[bytes: []byte | str](mut s: bytes, mut local: &Location): (Time,
}
}
ret t, true
}

fn parseStrictRFC3339(b: []byte)!: Time {
mut t, ok := parseRFC3339(b, unsafe { *(&Local) })
if !ok {
t = Parse(RFC3339, unsafe::BytesStr(b)) else { error(error) }
// The parse template syntax cannot correctly validate RFC 3339.
// Explicitly check for cases that Parse is unable to validate for.
num2 := fn(b: []byte): byte { ret 10*(b[0]-'0') + (b[1] - '0') }
match {
| true:
ret t
| b[len("2006-01-02T")+1] == ':': // hour must be two digits
error(ParseError.InvalidNumber)
| b[len("2006-01-02T15:04:05")] == ',': // sub-second separator must be a period
error(ParseError.BadField)
| b[len(b)-1] != 'Z':
match {
| num2(b[len(b)-len("07:00"):]) >= 24: // timezone hour must be in range
error(ParseError.InvalidRange)
| num2(b[len(b)-len("00"):]) >= 60: // timezone minute must be in range
error(ParseError.InvalidRange)
}
|:
// unknown error; should not occur
error(ParseError.BadField)
}
}
ret t
}
31 changes: 31 additions & 0 deletions std/time/time.jule
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,37 @@ impl Time {
year, yday := thu.yearYday()
ret year, (yday-1)/7 + 1
}

fn appendTo(self, mut b: []byte)!: []byte {
b, ok := appendStrictRFC3339(self, b)
if !ok {
error(ParseError.InvalidRange)
}
ret b
}

// Implements the custom text encoder method which is appends to b.
// The time is formatted in RFC 3339 format with sub-second precision.
// If the timestamp cannot be represented as valid RFC 3339
// (e.g., the year is out of range), then throws exception
// with the ParseError.InvalidRange.
fn AppendText(self, mut b: []byte)!: []byte {
ret self.appendTo(b) else { error(error) }
}

// Implements the custom text encoder method.
// matches that of calling the [Time.AppendText] method.
//
// See [Time.AppendText] for more information.
fn EncodeText(self)!: []byte {
ret self.appendTo(make([]byte, 0, len(RFC3339Nano))) else { error(error) }
}

// Implements the custom text decoder method.
// The time must be in the RFC 3339 format.
fn DecodeText(mut self, data: []byte)! {
self = parseStrictRFC3339(data) else { error(error) }
}
}

// Returns the current system-time UTC.
Expand Down

0 comments on commit 5122ce6

Please sign in to comment.