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

Format to string #1121

Closed
wants to merge 6 commits into from
Closed
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
104 changes: 97 additions & 7 deletions src/datetime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -765,17 +765,71 @@ where
DelayedFormat::new_with_offset(Some(local.date()), Some(local.time()), &self.offset, items)
}

/// Formats the combined date and time per the specified format string.
/// Formats the date and time with the specified format string.
///
/// See the [`crate::format::strftime`] module for the supported escape sequences.
/// See the [`format::strftime` module](crate::format::strftime) for the supported escape
/// sequences.
///
/// This returns a `DelayedFormat`, which gets converted to a string only when actual formatting
/// happens. You can feed this into [`print!`] and other formatting macros.
/// (This can avoid a memory allocation.)
///
/// If you want to format to a `String`, consider using the more direct method
/// [`format_to_string`](#method.format_to_string).
/// This is an alternative to calling [`ToString::to_string`] on the `DelayedFormat` returned by
/// this method.
///
/// # Errors/panics
///
/// This function does not panic or return an error.
///
/// However the `Display` implementation of `DelayedFormat` can return an error if the format
/// string is invalid. Returning an error goes against the [contract for `Display`][1]. This
/// will be fixed in the next major version of chrono.
///
/// Consumers of the `Display` trait, such as [`format!`], [`println!`] and [`to_string`] will
/// generally panic on an invalid formatting string. Consider using this function in combination
/// with formatting macro's that can pass on an error instead such as [`write!`] and
/// [`writeln!`].
///
/// [1]: https://doc.rust-lang.org/stable/std/fmt/index.html#formatting-traits
/// [`to_string`]: ToString::to_string
///
/// # Errors
///
/// This function does not panic or return an error. But the `Display` implementation can
/// return an error if the format string is invalid.
///
/// If you need to handle a potentially invalid format string, use this function in combination
/// with formatting macro's that can pass on an error instead of panicking, such as [`write!`]
/// and [`writeln!`], instead of [`format!`] and [`println!`].
///
/// # Example
/// ```rust
/// use chrono::prelude::*;
///
/// let date_time: DateTime<Utc> = Utc.with_ymd_and_hms(2017, 04, 02, 12, 50, 32).unwrap();
/// let formatted = format!("{}", date_time.format("%d/%m/%Y %H:%M"));
/// assert_eq!(formatted, "02/04/2017 12:50");
/// ```
/// use chrono::{TimeZone, Utc};
///
/// let dt = Utc.with_ymd_and_hms(2015, 9, 5, 23, 56, 4).unwrap();
/// assert_eq!(dt.format("%Y-%m-%d %H:%M:%S %Z").to_string(), "2015-09-05 23:56:04 UTC");
/// assert_eq!(dt.format("%Y-%m-%d %H:%M:%S %:z").to_string(), "2015-09-05 23:56:04 +00:00");
/// assert_eq!( dt.format("around %l %p on %b %-d").to_string(), "around 11 PM on Sep 5");
/// ```
///
/// The resulting `DelayedFormat` can be used directly with formatting macro's via the `Display`
/// trait.
///
/// ```
/// # use core::fmt::{Error, Write};
/// # use chrono::{TimeZone, Utc};
/// # let dt = Utc.with_ymd_and_hms(2015, 9, 5, 23, 56, 4).unwrap();
/// assert_eq!(format!("{}", dt.format("%Y-%m-%d %H:%M:%S %Z")), "2015-09-05 23:56:04 UTC");
///
/// let mut formatted = String::new();
/// write!(formatted, "{}", dt.format("around %l %p on %b %-d"))?;
/// assert_eq!(formatted, "around 11 PM on Sep 5");
///
/// println!("{}", dt.format("%Y-%m-%d %H:%M:%S %:z")); // prints "2015-09-05 23:56:04 +00:00"
/// # Ok::<(), Error>(())
/// ```
#[cfg(any(feature = "alloc", feature = "std", test))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))]
Expand All @@ -785,6 +839,42 @@ where
self.format_with_items(StrftimeItems::new(fmt))
}

/// Format the date and time with the specified format string directly to a `String`.
///
/// See the [`format::strftime` module](crate::format::strftime) for the supported escape
/// sequences.
///
/// # Errors
///
/// Returns an error if the format string is invalid.
///
/// # Example
///
/// ```
/// use chrono::{TimeZone, Utc};
///
/// let dt = Utc.with_ymd_and_hms(2015, 9, 5, 23, 56, 4).unwrap();
/// assert_eq!(
/// dt.format_to_string("%Y-%m-%d %H:%M:%S %Z"),
/// Ok("2015-09-05 23:56:04 UTC".to_owned())
/// );
/// assert_eq!(
/// dt.format_to_string("%Y-%m-%d %H:%M:%S %:z"),
/// Ok("2015-09-05 23:56:04 +00:00".to_owned())
/// );
/// assert_eq!(
/// dt.format_to_string("around %l %p on %b %-d"),
/// Ok("around 11 PM on Sep 5".to_owned())
/// );
/// ```
#[cfg(any(feature = "alloc", feature = "std", test))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))]
pub fn format_to_string(&self, fmt: &str) -> Result<String, fmt::Error> {
let mut s = String::new();
write!(s, "{}", self.format_with_items(StrftimeItems::new(fmt)))?;
Ok(s)
}

/// Formats the combined date and time with the specified formatting items and locale.
#[cfg(feature = "unstable-locales")]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-locales")))]
Expand Down
6 changes: 6 additions & 0 deletions src/datetime/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1022,3 +1022,9 @@ fn test_datetime_fixed_offset() {
let datetime_fixed = fixed_offset.from_local_datetime(&naivedatetime).unwrap();
assert_eq!(datetime_fixed.fixed_offset(), datetime_fixed);
}

#[test]
fn test_formatting_permissive_offset_no_panic() {
let dt = Utc.with_ymd_and_hms(2014, 11, 28, 12, 0, 9).unwrap();
assert!(dt.format_to_string("%#z").is_err());
}
2 changes: 1 addition & 1 deletion src/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@ fn format_inner(
off.map(|&(_, off)| write_local_minus_utc(result, off, true, Colons::None))
}
Internal(InternalFixed { val: InternalInternal::TimezoneOffsetPermissive }) => {
panic!("Do not try to write %#z it is undefined")
None // Do not try to write %#z it is undefined
}
RFC2822 =>
// same as `%a, %d %b %Y %H:%M:%S %z`
Expand Down
10 changes: 7 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,8 @@
//!
//! ### Formatting and Parsing
//!
//! Formatting is done via the [`format`](./struct.DateTime.html#method.format) method,
//! Formatting is done via the [`format`](./struct.DateTime.html#method.format) and
//! [`format_to_string`](./struct.DateTime.html#method.format_to_string) methods,
//! which format is equivalent to the familiar `strftime` format.
//!
//! See [`format::strftime`](./format/strftime/index.html#specifiers)
Expand Down Expand Up @@ -233,8 +234,11 @@
//! # fn test() {
//! let dt = Utc.with_ymd_and_hms(2014, 11, 28, 12, 0, 9).unwrap();
//! assert_eq!(dt.format("%Y-%m-%d %H:%M:%S").to_string(), "2014-11-28 12:00:09");
//! assert_eq!(dt.format("%a %b %e %T %Y").to_string(), "Fri Nov 28 12:00:09 2014");
//! assert_eq!(dt.format_localized("%A %e %B %Y, %T", Locale::fr_BE).to_string(), "vendredi 28 novembre 2014, 12:00:09");
//! assert_eq!(dt.format_to_string("%a %b %e %T %Y"), Ok("Fri Nov 28 12:00:09 2014".to_owned()));
//! assert_eq!(
//! dt.format_localized("%A %e %B %Y, %T", Locale::fr_BE).to_string(),
//! "vendredi 28 novembre 2014, 12:00:09"
//! );
//!
//! assert_eq!(dt.format("%a %b %e %T %Y").to_string(), dt.format("%c").to_string());
//! assert_eq!(dt.to_string(), "2014-11-28 12:00:09 UTC");
Expand Down
87 changes: 73 additions & 14 deletions src/naive/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@

//! ISO 8601 calendar date without timezone.

#[cfg(feature = "alloc")]
extern crate alloc;

#[cfg(all(not(feature = "std"), feature = "alloc"))]
use alloc::string::String;
#[cfg(any(feature = "alloc", feature = "std", test))]
use core::borrow::Borrow;
use core::fmt::Write;
use core::ops::{Add, AddAssign, RangeInclusive, Sub, SubAssign};
use core::{fmt, str};

Expand Down Expand Up @@ -1210,18 +1216,35 @@ impl NaiveDate {
}

/// Formats the date with the specified format string.
/// See the [`format::strftime` module](../format/strftime/index.html)
/// on the supported escape sequences.
///
/// This returns a `DelayedFormat`,
/// which gets converted to a string only when actual formatting happens.
/// You may use the `to_string` method to get a `String`,
/// or just feed it into `print!` and other formatting macros.
/// (In this way it avoids the redundant memory allocation.)
/// See the [`format::strftime` module](crate::format::strftime) for the supported escape
/// sequences.
///
/// This returns a `DelayedFormat`, which gets converted to a string only when actual formatting
/// happens. You can feed this into [`print!`] and other formatting macros.
/// (This can avoid a memory allocation.)
///
/// If you want to format to a `String`, consider using the more direct method
/// [`format_to_string`](#method.format_to_string).
/// This is an alternative to calling [`ToString::to_string`] on the `DelayedFormat` returned by
/// this method.
///
/// # Errors/panics
///
/// This function does not panic or return an error.
///
/// A wrong format string does *not* issue an error immediately.
/// Rather, converting or formatting the `DelayedFormat` fails.
/// You are recommended to immediately use `DelayedFormat` for this reason.
/// However the `Display` implementation of `DelayedFormat` can return an error if the format
/// string is invalid, or if the format string contains time or offset fields (which a
/// `NaiveDate` can not provide). Returning an error goes against the
/// [contract for `Display`][1]. This will be fixed in the next major version of chrono.
///
/// Consumers of the `Display` trait, such as [`format!`], [`println!`] and [`to_string`] will
/// generally panic on an invalid formatting string. Consider using this function in combination
/// with formatting macro's that can pass on an error instead such as [`write!`] and
/// [`writeln!`].
///
/// [1]: https://doc.rust-lang.org/stable/std/fmt/index.html#formatting-traits
/// [`to_string`]: ToString::to_string
///
/// # Example
///
Expand All @@ -1233,13 +1256,21 @@ impl NaiveDate {
/// assert_eq!(d.format("%A, %-d %B, %C%y").to_string(), "Saturday, 5 September, 2015");
/// ```
///
/// The resulting `DelayedFormat` can be formatted directly via the `Display` trait.
/// The resulting `DelayedFormat` can be used directly with formatting macro's via the `Display`
/// trait.
///
/// ```
/// # use core::fmt::{Error, Write};
/// # use chrono::NaiveDate;
/// # let d = NaiveDate::from_ymd_opt(2015, 9, 5).unwrap();
/// assert_eq!(format!("{}", d.format("%Y-%m-%d")), "2015-09-05");
/// assert_eq!(format!("{}", d.format("%A, %-d %B, %C%y")), "Saturday, 5 September, 2015");
///
/// let mut formatted = String::new();
/// write!(formatted, "{}", d.format("%A, %-d %B, %C%y"))?;
/// assert_eq!(formatted, "Saturday, 5 September, 2015");
///
/// println!("{}", d.format("%Y-%m-%d")); // prints "2015-09-05"
/// # Ok::<(), Error>(())
/// ```
#[cfg(any(feature = "alloc", feature = "std", test))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))]
Expand All @@ -1249,6 +1280,36 @@ impl NaiveDate {
self.format_with_items(StrftimeItems::new(fmt))
}

/// Format the date with the specified format string directly to a `String`.
///
/// See the [`format::strftime` module](crate::format::strftime) for the supported escape
/// sequences.
///
/// # Errors
///
/// Returns an error if the format string is invalid, or if the format string contains time or
/// offset fields (which a `NaiveDate` can not provide).
///
/// # Example
///
/// ```
/// use chrono::NaiveDate;
///
/// let d = NaiveDate::from_ymd_opt(2015, 9, 5).unwrap();
/// assert_eq!(d.format_to_string("%Y-%m-%d"), Ok("2015-09-05".to_owned()));
/// assert_eq!(
/// d.format_to_string("%A, %-d %B, %C%y"),
/// Ok("Saturday, 5 September, 2015".to_owned())
/// );
/// ```
#[cfg(any(feature = "alloc", feature = "std", test))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))]
pub fn format_to_string(&self, fmt: &str) -> Result<String, fmt::Error> {
let mut s = String::new();
write!(s, "{}", self.format_with_items(StrftimeItems::new(fmt)))?;
Ok(s)
}

/// Formats the date with the specified formatting items and locale.
#[cfg(feature = "unstable-locales")]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-locales")))]
Expand Down Expand Up @@ -1992,8 +2053,6 @@ impl DoubleEndedIterator for NaiveDateWeeksIterator {
/// ```
impl fmt::Debug for NaiveDate {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use core::fmt::Write;

let year = self.year();
let mdf = self.mdf();
if (0..=9999).contains(&year) {
Expand Down
Loading