Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for RFC 2822 in DateTime #285

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions crates/lune-std-datetime/src/date_time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,21 @@ impl DateTime {
Ok(Self { inner })
}

/**
Parses a time string in the RFC 2822 format, such as
`Tue, 1 Jul 2003 10:52:37 +0200`, into a new `DateTime` struct.

See [`chrono::DateTime::parse_from_rfc2822`] for additional details.

# Errors

Returns an error if the input string is not a valid RFC 2822 date-time.
*/
pub fn from_rfc_date(rfc_date: impl AsRef<str>) -> DateTimeResult<Self> {
let inner = ChronoDateTime::parse_from_rfc2822(rfc_date.as_ref())?.with_timezone(&Utc);
Ok(Self { inner })
}

/**
Extracts individual date & time values from this
`DateTime`, using the current local time zone.
Expand Down Expand Up @@ -200,6 +215,16 @@ impl DateTime {
pub fn to_iso_date(self) -> String {
self.inner.to_rfc3339()
}

/**
Formats a time string in the RFC 2822 format, such as `Tue, 1 Jul 2003 10:52:37 +0200`.

See [`chrono::DateTime::to_rfc2822`] for additional details.
*/
#[must_use]
pub fn to_rfc_date(self) -> String {
self.inner.to_rfc2822()
}
}

impl LuaUserData for DateTime {
Expand Down Expand Up @@ -230,6 +255,7 @@ impl LuaUserData for DateTime {
);
// Normal methods
methods.add_method("toIsoDate", |_, this, ()| Ok(this.to_iso_date()));
methods.add_method("toRfcDate", |_, this, ()| Ok(this.to_rfc_date()));
methods.add_method(
"formatUniversalTime",
|_, this, (format, locale): (Option<String>, Option<String>)| {
Expand Down
3 changes: 3 additions & 0 deletions crates/lune-std-datetime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
.with_function("fromIsoDate", |_, iso_date: String| {
Ok(DateTime::from_iso_date(iso_date)?)
})?
.with_function("fromRfcDate", |_, rfc_date: String| {
Ok(DateTime::from_rfc_date(rfc_date)?)
})?
.with_function("fromLocalTime", |_, values| {
Ok(DateTime::from_local_time(&values)?)
})?
Expand Down
2 changes: 2 additions & 0 deletions crates/lune/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,13 @@ create_tests! {
datetime_format_local_time: "datetime/formatLocalTime",
datetime_format_universal_time: "datetime/formatUniversalTime",
datetime_from_iso_date: "datetime/fromIsoDate",
datetime_from_rfc_date: "datetime/fromRfcDate",
datetime_from_local_time: "datetime/fromLocalTime",
datetime_from_universal_time: "datetime/fromUniversalTime",
datetime_from_unix_timestamp: "datetime/fromUnixTimestamp",
datetime_now: "datetime/now",
datetime_to_iso_date: "datetime/toIsoDate",
datetime_to_rfc_date: "datetime/toRfcDate",
datetime_to_local_time: "datetime/toLocalTime",
datetime_to_universal_time: "datetime/toUniversalTime",
}
Expand Down
11 changes: 11 additions & 0 deletions tests/datetime/fromRfcDate.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
local DateTime = require("@lune/datetime")

assert(
DateTime.fromRfcDate("Fri, 21 Nov 1997 09:55:06 -0600") ~= nil,
"expected DateTime.fromRfcDate() to return DateTime, got nil"
)

assert(
DateTime.fromRfcDate("Tue, 1 Jul 2003 10:52:37 +0200") ~= nil,
"expected DateTime.fromRfcDate() to return DateTime, got nil"
)
51 changes: 51 additions & 0 deletions tests/datetime/toRfcDate.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
local DateTime = require("@lune/datetime")

local now = DateTime.now()
local nowRfc = now:toRfcDate()

assert(type(nowRfc) == "string", "toRfcDate should return a string")
assert(
string.match(nowRfc, "^%a%a%a, %d%d? %a%a%a %d%d%d%d %d%d:%d%d:%d%d [+-]%d%d%d%d$"),
"RFC 2822 date string does not match expected format"
)

-- Extract components of the RFC 2822 string
local day, date, month, year, time, timezone =
nowRfc:match("^(%a%a%a), (%d%d?) (%a%a%a) (%d%d%d%d) (%d%d:%d%d:%d%d) ([+-]%d%d%d%d)$")

if not day or not date or not month or not year or not time or not timezone then
error("Failed to extract components from RFC 2822 date string")
end

-- Validate month
local validMonths = {
Jan = true, Feb = true, Mar = true, Apr = true, May = true, Jun = true,
Jul = true, Aug = true, Sep = true, Oct = true, Nov = true, Dec = true,
}
assert(validMonths[month], "Month must be a valid RFC 2822 month abbreviation")

-- Validate year
assert(string.match(year, "^%d%d%d%d$"), "Year must be a 4-digit number")

-- Validate date
local dayNum = tonumber(date)
assert(dayNum >= 1 and dayNum <= 31, "Date must be between 1 and 31")

-- Validate time
local hour, minute, second = time:match("^(%d%d):(%d%d):(%d%d)$")
if not hour or not minute or not second then
error("Failed to extract time components from RFC 2822 date string")
end

assert(hour and tonumber(hour) >= 0 and tonumber(hour) < 24, "Hour must be between 0 and 23")
assert(minute and tonumber(minute) >= 0 and tonumber(minute) < 60, "Minute must be between 0 and 59")
assert(second and tonumber(second) >= 0 and tonumber(second) < 60, "Second must be between 0 and 59")

-- Validate timezone
local tzHour, tzMinute = timezone:match("^([+-]%d%d)(%d%d)$")
if not tzHour or not tzMinute then
error("Failed to extract timezone components from RFC 2822 date string")
end

assert(tzHour and tonumber(tzHour) >= -14 and tonumber(tzHour) <= 14, "Timezone hour offset must be between -14 and +14")
assert(tzMinute and tonumber(tzMinute) >= 0 and tonumber(tzMinute) < 60, "Timezone minute offset must be between 0 and 59")
45 changes: 45 additions & 0 deletions types/datetime.luau
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,24 @@ function DateTime.toIsoDate(self: DateTime): string
return nil :: any
end

--[=[
@within DateTime
@tag Method

Formats this `DateTime` as an RFC 2822 date-time string.

Some examples of RFC 2822 date-time strings are:

- `Fri, 21 Nov 1997 09:55:06 -0600`
- `Tue, 1 Jul 2003 10:52:37 +0200`
- `Mon, 23 Dec 2024 01:58:48 GMT`

@return string -- The RFC 2822 formatted string
]=]
function DateTime.toRfcDate(self: DateTime): string
return nil :: any
end

--[=[
@within DateTime
@tag Method
Expand Down Expand Up @@ -255,6 +273,9 @@ export type DateTime = typeof(DateTime)
-- Formats the current moment in time as an ISO 8601 string
print(now:toIsoDate())

-- Formats the current moment in time as an RFC 2822 string
print(now:toRfcDate())

-- Formats the current moment in time, using the local
-- time, the French locale, and the specified time string
print(now:formatLocalTime("%A, %d %B %Y", "fr"))
Expand Down Expand Up @@ -414,4 +435,28 @@ function dateTime.fromIsoDate(isoDate: string): DateTime
return nil :: any
end

--[=[
@within DateTime
@tag Constructor

Creates a new `DateTime` from an RFC 2822 date-time string.

### Errors

This constructor is fallible and may throw an error if the given
string does not strictly follow the RFC 2822 date-time string format.

Some examples of valid RFC 2822 date-time strings are:

- `Fri, 21 Nov 1997 09:55:06 -0600`
- `Tue, 1 Jul 2003 10:52:37 +0200`
- `Mon, 23 Dec 2024 01:58:48 GMT`

@param rfcDate -- An RFC 2822 formatted string
@return DateTime -- The new DateTime object
]=]
function dateTime.fromRfcDate(rfcDate: string): DateTime
return nil :: any
end

return dateTime