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 backwards compatible support for fmt::Write #407

Closed
wants to merge 12 commits into from
19 changes: 19 additions & 0 deletions src/date.rs
Expand Up @@ -824,6 +824,25 @@ impl Date {
output: &mut impl io::Write,
format: &(impl Formattable + ?Sized),
) -> Result<usize, error::Format> {
format.format_into_old(output, Some(self), None, None)
}

/// Format the `Date` using the provided [format description](crate::format_description).
///
/// Exactly like [`Date::format_into`] but accepts a [`fmt::Write`] instead of an [`io::Write`]
/// ```rust
/// # use time::{format_description, macros::date};
/// let format = format_description::parse("[year]-[month]-[day]")?;
/// let mut buf = String::new();
/// date!(2020 - 01 - 02).format_into_fmt_writer(&mut buf, &format)?;
/// assert_eq!(buf, "2020-01-02");
/// # Ok::<_, time::Error>(())
/// ```
pub fn format_into_fmt_writer(
self,
output: &mut impl fmt::Write,
format: &(impl Formattable + ?Sized),
) -> Result<(), error::Format> {
format.format_into(output, Some(self), None, None)
}

Expand Down
10 changes: 10 additions & 0 deletions src/error/format.rs
Expand Up @@ -21,6 +21,8 @@ pub enum Format {
InvalidComponent(&'static str),
/// A value of `std::io::Error` was returned internally.
StdIo(io::Error),
/// A value of `std::fmt::Error` was returned internally.
StdFmt(fmt::Error),
}

impl fmt::Display for Format {
Expand All @@ -36,6 +38,7 @@ impl fmt::Display for Format {
component
),
Self::StdIo(err) => err.fmt(f),
Self::StdFmt(err) => err.fmt(f),
}
}
}
Expand All @@ -46,6 +49,12 @@ impl From<io::Error> for Format {
}
}

impl From<fmt::Error> for Format {
fn from(err: fmt::Error) -> Self {
Self::StdFmt(err)
}
}

impl TryFrom<Format> for io::Error {
type Error = error::DifferentVariant;

Expand All @@ -63,6 +72,7 @@ impl std::error::Error for Format {
match *self {
Self::InsufficientTypeInformation | Self::InvalidComponent(_) => None,
Self::StdIo(ref err) => Some(err),
Self::StdFmt(ref err) => Some(err),
}
}
}
Expand Down
186 changes: 112 additions & 74 deletions src/formatting/formattable.rs
@@ -1,7 +1,7 @@
//! A trait that can be used to format an item from its components.

use core::ops::Deref;
use std::io;
use std::{fmt, io};

use crate::format_description::well_known::{Rfc2822, Rfc3339};
use crate::format_description::FormatItem;
Expand All @@ -19,6 +19,31 @@ impl Formattable for Rfc3339 {}
impl Formattable for Rfc2822 {}
impl<T: Deref> Formattable for T where T::Target: Formattable {}

/// A compatibility layer to translate [`io::Write`] into [`fmt::Write`]
struct Compat<'a, W: io::Write> {
/// The [`io::Write`]r to apply the translation on
writer: &'a mut W,
/// The total bytes written into the writer so far
bytes_written: usize,
/// The last error from the writer if it returned errors
error: Option<io::Error>,
}

impl<'a, W> fmt::Write for Compat<'a, W>
where
W: io::Write,
{
fn write_str(&mut self, s: &str) -> fmt::Result {
self.writer
.write_all(s.as_bytes())
.map(|_| self.bytes_written += s.len())
.map_err(|error| {
self.error = Some(error);
fmt::Error
})
}
}

/// Seal the trait to prevent downstream users from implementing it.
mod sealed {
#[allow(clippy::wildcard_imports)]
Expand All @@ -27,14 +52,37 @@ mod sealed {
/// Format the item using a format description, the intended output, and the various components.
#[cfg_attr(__time_03_docs, doc(cfg(feature = "formatting")))]
pub trait Sealed {
/// Format the item into the provided output, returning the number of bytes written.
/// Format the item into the provided output.
fn format_into(
&self,
output: &mut impl fmt::Write,
date: Option<Date>,
time: Option<Time>,
offset: Option<UtcOffset>,
) -> Result<(), error::Format>;

/// Format the item into the provided output, returning the number of bytes written.
fn format_into_old(
&self,
output: &mut impl io::Write,
date: Option<Date>,
time: Option<Time>,
offset: Option<UtcOffset>,
) -> Result<usize, error::Format>;
) -> Result<usize, error::Format> {
let mut compat = Compat {
writer: output,
bytes_written: 0,
error: None,
};
self.format_into(&mut compat, date, time, offset)
.map(|_| compat.bytes_written)
.map_err(|fmt_error| {
compat
.error
.map(error::Format::from)
.unwrap_or_else(|| fmt_error)
})
}

/// Format the item directly to a `String`.
fn format(
Expand All @@ -43,9 +91,9 @@ mod sealed {
time: Option<Time>,
offset: Option<UtcOffset>,
) -> Result<String, error::Format> {
let mut buf = Vec::new();
let mut buf = String::new();
self.format_into(&mut buf, date, time, offset)?;
Ok(String::from_utf8_lossy(&buf).into_owned())
Ok(buf)
}
}
}
Expand All @@ -54,37 +102,40 @@ mod sealed {
impl<'a> sealed::Sealed for FormatItem<'a> {
fn format_into(
&self,
output: &mut impl io::Write,
output: &mut impl fmt::Write,
date: Option<Date>,
time: Option<Time>,
offset: Option<UtcOffset>,
) -> Result<usize, error::Format> {
Ok(match *self {
Self::Literal(literal) => write(output, literal)?,
) -> Result<(), error::Format> {
match *self {
Self::Literal(literal) => {
output.write_str(String::from_utf8_lossy(literal).as_ref())?
Mathspy marked this conversation as resolved.
Show resolved Hide resolved
}
Self::Component(component) => format_component(output, component, date, time, offset)?,
Self::Compound(items) => items.format_into(output, date, time, offset)?,
Self::Optional(item) => item.format_into(output, date, time, offset)?,
Self::First(items) => match items {
[] => 0,
[] => (),
[item, ..] => item.format_into(output, date, time, offset)?,
},
})
};

Ok(())
}
}

impl<'a> sealed::Sealed for [FormatItem<'a>] {
fn format_into(
&self,
output: &mut impl io::Write,
output: &mut impl fmt::Write,
date: Option<Date>,
time: Option<Time>,
offset: Option<UtcOffset>,
) -> Result<usize, error::Format> {
let mut bytes = 0;
) -> Result<(), error::Format> {
for item in self.iter() {
bytes += item.format_into(output, date, time, offset)?;
item.format_into(output, date, time, offset)?;
}
Ok(bytes)
Ok(())
}
}

Expand All @@ -94,11 +145,11 @@ where
{
fn format_into(
&self,
output: &mut impl io::Write,
output: &mut impl fmt::Write,
date: Option<Date>,
time: Option<Time>,
offset: Option<UtcOffset>,
) -> Result<usize, error::Format> {
) -> Result<(), error::Format> {
self.deref().format_into(output, date, time, offset)
}
}
Expand All @@ -108,17 +159,15 @@ where
impl sealed::Sealed for Rfc2822 {
fn format_into(
&self,
output: &mut impl io::Write,
output: &mut impl fmt::Write,
date: Option<Date>,
time: Option<Time>,
offset: Option<UtcOffset>,
) -> Result<usize, error::Format> {
) -> Result<(), error::Format> {
let date = date.ok_or(error::Format::InsufficientTypeInformation)?;
let time = time.ok_or(error::Format::InsufficientTypeInformation)?;
let offset = offset.ok_or(error::Format::InsufficientTypeInformation)?;

let mut bytes = 0;

let (year, month, day) = date.to_calendar_date();

if year < 1900 {
Expand All @@ -128,46 +177,43 @@ impl sealed::Sealed for Rfc2822 {
return Err(error::Format::InvalidComponent("offset_second"));
}

bytes += write(
write(
output,
&WEEKDAY_NAMES[date.weekday().number_days_from_monday() as usize][..3],
)?;
bytes += write(output, b", ")?;
bytes += format_number_pad_zero::<_, _, 2>(output, day)?;
bytes += write(output, b" ")?;
bytes += write(output, &MONTH_NAMES[month as usize - 1][..3])?;
bytes += write(output, b" ")?;
bytes += format_number_pad_zero::<_, _, 4>(output, year as u32)?;
bytes += write(output, b" ")?;
bytes += format_number_pad_zero::<_, _, 2>(output, time.hour())?;
bytes += write(output, b":")?;
bytes += format_number_pad_zero::<_, _, 2>(output, time.minute())?;
bytes += write(output, b":")?;
bytes += format_number_pad_zero::<_, _, 2>(output, time.second())?;
bytes += write(output, b" ")?;
bytes += write(output, if offset.is_negative() { b"-" } else { b"+" })?;
bytes += format_number_pad_zero::<_, _, 2>(output, offset.whole_hours().unsigned_abs())?;
bytes +=
format_number_pad_zero::<_, _, 2>(output, offset.minutes_past_hour().unsigned_abs())?;

Ok(bytes)
write(output, ", ")?;
format_number_pad_zero::<_, _, 2>(output, day)?;
write(output, " ")?;
write(output, &MONTH_NAMES[month as usize - 1][..3])?;
write(output, " ")?;
format_number_pad_zero::<_, _, 4>(output, year as u32)?;
write(output, " ")?;
format_number_pad_zero::<_, _, 2>(output, time.hour())?;
write(output, ":")?;
format_number_pad_zero::<_, _, 2>(output, time.minute())?;
write(output, ":")?;
format_number_pad_zero::<_, _, 2>(output, time.second())?;
write(output, " ")?;
write(output, if offset.is_negative() { "-" } else { "+" })?;
format_number_pad_zero::<_, _, 2>(output, offset.whole_hours().unsigned_abs())?;
format_number_pad_zero::<_, _, 2>(output, offset.minutes_past_hour().unsigned_abs())?;

Ok(())
}
}

impl sealed::Sealed for Rfc3339 {
fn format_into(
&self,
output: &mut impl io::Write,
output: &mut impl fmt::Write,
date: Option<Date>,
time: Option<Time>,
offset: Option<UtcOffset>,
) -> Result<usize, error::Format> {
) -> Result<(), error::Format> {
let date = date.ok_or(error::Format::InsufficientTypeInformation)?;
let time = time.ok_or(error::Format::InsufficientTypeInformation)?;
let offset = offset.ok_or(error::Format::InsufficientTypeInformation)?;

let mut bytes = 0;

let year = date.year();

if !(0..10_000).contains(&year) {
Expand All @@ -177,23 +223,23 @@ impl sealed::Sealed for Rfc3339 {
return Err(error::Format::InvalidComponent("offset_second"));
}

bytes += format_number_pad_zero::<_, _, 4>(output, year as u32)?;
bytes += write(output, &[b'-'])?;
bytes += format_number_pad_zero::<_, _, 2>(output, date.month() as u8)?;
bytes += write(output, &[b'-'])?;
bytes += format_number_pad_zero::<_, _, 2>(output, date.day())?;
bytes += write(output, &[b'T'])?;
bytes += format_number_pad_zero::<_, _, 2>(output, time.hour())?;
bytes += write(output, &[b':'])?;
bytes += format_number_pad_zero::<_, _, 2>(output, time.minute())?;
bytes += write(output, &[b':'])?;
bytes += format_number_pad_zero::<_, _, 2>(output, time.second())?;
format_number_pad_zero::<_, _, 4>(output, year as u32)?;
write(output, "-")?;
format_number_pad_zero::<_, _, 2>(output, date.month() as u8)?;
write(output, "-")?;
format_number_pad_zero::<_, _, 2>(output, date.day())?;
write(output, "T")?;
format_number_pad_zero::<_, _, 2>(output, time.hour())?;
write(output, ":")?;
format_number_pad_zero::<_, _, 2>(output, time.minute())?;
write(output, ":")?;
format_number_pad_zero::<_, _, 2>(output, time.second())?;

#[allow(clippy::if_not_else)]
if time.nanosecond() != 0 {
let nanos = time.nanosecond();
bytes += write(output, &[b'.'])?;
bytes += if nanos % 10 != 0 {
write(output, ".")?;
if nanos % 10 != 0 {
format_number_pad_zero::<_, _, 9>(output, nanos)
} else if (nanos / 10) % 10 != 0 {
format_number_pad_zero::<_, _, 8>(output, nanos / 10)
Expand All @@ -215,24 +261,16 @@ impl sealed::Sealed for Rfc3339 {
}

if offset == UtcOffset::UTC {
bytes += write(output, &[b'Z'])?;
return Ok(bytes);
write(output, "Z")?;
return Ok(());
}

bytes += write(
output,
if offset.is_negative() {
&[b'-']
} else {
&[b'+']
},
)?;
bytes += format_number_pad_zero::<_, _, 2>(output, offset.whole_hours().unsigned_abs())?;
bytes += write(output, &[b':'])?;
bytes +=
format_number_pad_zero::<_, _, 2>(output, offset.minutes_past_hour().unsigned_abs())?;
write(output, if offset.is_negative() { "-" } else { "+" })?;
format_number_pad_zero::<_, _, 2>(output, offset.whole_hours().unsigned_abs())?;
write(output, ":")?;
format_number_pad_zero::<_, _, 2>(output, offset.minutes_past_hour().unsigned_abs())?;

Ok(bytes)
Ok(())
}
}
// endregion well-known formats