
Вы когда-нибудь хотели прогуляться по трёхмерному фракталу? Что ж, теперь вы можете.
Всё ещё в разработке, но выглядит потрясающе.
https://bananaft.itch.io/yedomaglobula
Size: a a a
String
со датой, отформатированной согласно переданному формату. Выглядит это примерно так (пример из документации):assert_eq!(date!(2019-01-02).format("%Y-%m-%d"), "2019-01-02");Выглядит неплохо, но будем честны: в подавляющем большинстве случаев строка формата так и остаётся литералом. Метод же, тем не менее, вынужден парсить строку при каждом вызове, и лично я сомневаюсь, что этот код будет специализирован на этапе компиляции (всё-таки rustc не является суперкомпилятором). Многократная компиляция регулярных выражений является известным антипаттерном, и для решения этой проблемы есть инструмент, а для формата даты такого инструмента нет. Сегодня мы напишем подобный инструмент сами.
use std::fmt;Сам общий интерфейс форматировщиков:
#[derive(Default)]
pub struct Buf {
inner: String,
}
impl Buf {
pub fn new() -> Self {
Self::default()
}
pub fn append(&mut self, s: &str) {
self.inner += s;
}
// Этот метод позволит нам использовать макрос `write!` на `Buf`.
// Он позволяет не безусловно выделять новую строку,
// а использовать место в уже имеющейся
pub fn write_fmt(&mut self, args: fmt::Arguments) -> fmt::Result {
self.inner.write_fmt(args)
}
}
use std::fmt;(проблем с тем, чтобы принимать
use time::Date;
pub trait FormatDate {
fn format_date(&self, b: &mut Buf, d: Date) -> fmt::Result;
}
Date
по значению, нет, потому что это Copy
-тип).impl FormatDate for &'_ str {Теперь напишем реализацию для, скажем, дня даты:
fn format_date(&self, b: &mut Buf, _: Date) -> fmt::Result {
b.append(self);
Ok(())
}
}
pub struct DayOf;Формат
impl FormatDate for DayOf {
fn format_date(&self, b: &mut Buf, d: Date) -> fmt::Result {
write!(b, "{:02}", d.day())
}
}
"{:02}"
означает, что для печати дня отведено два места, и если в номере дня всего одна цифра, то вывод будет дополнен слева нулями. Форматировщики для номера месяца и года пишутся аналогично, поэтому не будем подробнее на этом останавливаться.macro_rules! impl_for_tuples {Добавим для удобства extension trait:
() => {};
($head:ident $(, $rest:ident)*) => {
impl<$head, $($rest),*> FormatDate for ($head, $($rest,)*)
where
$head: FormatDate,
$($rest: FormatDate,)*
{
fn format_date(&self, b: &mut Buf, date: Date) -> fmt::Result {
#[allow(non_snake_case)]
let &(ref $head, $(ref $rest,)*) = self;
$head.format_date(b, date)?;
$($rest.format_date(b, date)?;)*
Ok(())
}
}
impl_for_tuples!($($rest),*);
};
}
impl_for_tuples!(A, B, C, D, E, F, G, H);
pub trait DateExt {И проверим, как работает:
fn format_into<F: FormatDate>(self, b: &mut Buf, f: F) -> fmt::Result;
fn format_as<F: FormatDate>(self, f: F) -> String;
}
impl DateExt for Date {
fn format_into<F: FormatDate>(self, b: &mut Buf, f: F) -> fmt::Result {
f.format_date(b, self)
}
fn format_as<F: FormatDate>(self, f: F) -> String {
let mut buf = Buf::default();
let _ = self.format_into(&mut buf, f);
buf.inner
}
}
use time::date;Работает!
// Очень удобный макрос
let d = date!(2020-02-18);
let format = (DayOf, "/", MonthOf, "/", YearOf);
// Обратите внимание на ноль впереди в месяце
assert_eq!(d.format_as(format), "18/02/2020");
macro_rules! impl_for_tuples {Добавим для удобства extension trait:
() => {};
($head:ident $(, $rest:ident)*) => {
impl<$head, $($rest),*> FormatDate for ($head, $($rest,)*)
where
$head: FormatDate,
$($rest: FormatDate,)*
{
fn format_date(&self, b: &mut Buf, date: Date) -> fmt::Result {
#[allow(non_snake_case)]
let &(ref $head, $(ref $rest,)*) = self;
$head.format_date(b, date)?;
$($rest.format_date(b, date)?;)*
Ok(())
}
}
impl_for_tuples!($($rest),*);
};
}
impl_for_tuples!(A, B, C, D, E, F, G, H);
pub trait DateExt {И проверим, как работает:
fn format_into<F: FormatDate>(self, b: &mut Buf, f: F) -> fmt::Result;
fn format_as<F: FormatDate>(self, f: F) -> String;
}
impl DateExt for Date {
fn format_into<F: FormatDate>(self, b: &mut Buf, f: F) -> fmt::Result {
f.format_date(b, self)
}
fn format_as<F: FormatDate>(self, f: F) -> String {
let mut buf = Buf::default();
let _ = self.format_into(&mut buf, f);
buf.inner
}
}
use time::date;Работает!
// Очень удобный макрос
let d = date!(2020-02-18);
let format = (DayOf, "/", MonthOf, "/", YearOf);
// Обратите внимание на ноль впереди в месяце
assert_eq!(d.format_as(format), "18/02/2020");
pub trait FormatDate<D> {Для удобства сделаем алиас на возвращаемый тип Date::as_ymd:
fn format_date(&self, b: &mut Buf, d: D) -> fmt::Result;
}
pub type Ymd = (i32, u8, u8);
. Если тип умеет форматировать дату в деконструированном виде (Ymd
), то он может форматировать и исходную дату:impl<T: FormatDate<Ymd>> FormatDate<Date> for T {С первыми двумя недочётами разобраться несколько сложнее. Посмотрим на то, как, по идее, выглядит реализация
fn format_date(&self, b: &mut Buf, date: Date) -> fmt::Result {
self.format_date(b, date.as_ymd())
}
}
FormatDate<Ymd>
:impl FormatDate<Ymd> for SomeFormatter {(справка по синтаксису форматных строк)
fn format_date(&self, b: &mut Buf, ymd: Ymd) -> fmt::Result {
let part = some_part_of_date(ymd);
write!(b, "{:0width$}", part, width = SOME_WIDTH)
}
}
Display
;pub trait Extract<Date = Ymd> {Пример реализации:
type Output: fmt::Display;
fn extract(ymd: Date) -> Self::Output;
}
pub struct DayOf;Немного более сложный пример:
impl Extract for DayOf {
type Output = u8;
fn extract((_year, _month, day): Ymd) -> u8 {
day
}
}
pub struct MonthFullWordOf;Ширина поля, по хорошему, должна быть константой, но параметризовывать типы значениями в Rust на stable пока нельзя. Не то, чтобы меня это остановило, но в данном случае проблемы решается достаточно просто и без const generics:
impl Extract for MonthFullWordOf {
type Output = &'static str;
fn extract((_year, month, _day): Ymd) -> &'static str {
match month {
1 => "January",
2 => "February",
3 => "March",
4 => "April",
5 => "May",
6 => "Juny",
7 => "July",
8 => "August",
9 => "September",
10 => "October",
11 => "November",
12 => "December",
// Здесь нормально паниковать, потому что месяц другие номера иметь не может.
// В типах это, к сожалению, не выражено.
_ => unreachable!(),
}
}
}
pub trait Width {Наш составной форматировщик в итоге выглядит следующим образом:
const WIDTH: usize;
}
// Пример реализации
pub struct W2;
impl Width for W2 {
const WIDTH: usize = 2;
}
pub struct Formatter<Extractor, Width, Padding = NoPad>(
std::marker::PhantomData<(Extractor, Width, Padding)>,
);
FormatDate
для Formatter
, параметризованных различными заполнениями:pub struct PadZeros;Итак, мы приобрели в модульности, но что мы потеряли? Удобство использования! Каждый конкретный форматировщик теперь содержит поле, поэтому проинициализировать его просто по имени уже не получится. К счастью, это обходится достаточно просто: достаточно завести константы с нужными именами:
pub struct NoPad;
impl<Extractor, W> FormatDate<Ymd> for Formatter<Extractor, W, PadZeros>
where
Extractor: Extract,
W: Width,
{
fn format_date(&self, b: &mut Buf, ymd: Ymd) -> fmt::Result {
let part = Extractor::extract(ymd);
write!(b, "{:01$}", part, W::WIDTH)
}
}
impl<Extractor, W> FormatDate<Ymd> for Formatter<Extractor, W, NoPad>
where
Extractor: Extract,
W: Width,
{
fn format_date(&self, b: &mut Buf, ymd: Ymd) -> fmt::Result {
let part = Extractor::extract(ymd);
write!(b, "{:1$}", part, W::WIDTH)
}
}
pub type Day = Formatter<DayOf, W2, PadZeros>;Проверим:
pub const DAY: Day = Formatter(std::marker::PhantomData);
// Аналогично с остальными форматировщиками
let d = date!(2020-02-18);Работает!
let format = (DAY, "/", MONTH, "/", YEAR);
assert_eq!(d.format_as(format), "18/02/2020");
pub struct Nil;Разумеется, составлять подобный список руками куда менее удобно, чем кортеж — но мы и не будем! Вместо этого мы сделаем макрос, который будет конструировать список за нас. Мы несколько ужесточим требования к формату — теперь вместо произвольных выражений можно использовать лишь имена и литералы — но это упростит видимый синтаксис, потому что позволяет избавиться от запятых. Сам макрос:
pub struct Cons<T, U>(pub T, pub U);
impl FormatDate<Ymd> for Nil {
fn format_date(&self, _: &mut Buf, _: Ymd) -> fmt::Result {
Ok(())
}
}
impl<T, U> FormatDate<Ymd> for Cons<T, U>
where
T: FormatDate<Ymd>,
U: FormatDate<Ymd>,
{
fn format_date(&self, b: &mut Buf, ymd: Ymd) -> fmt::Result {
self.0.format_date(b, ymd)?;
self.1.format_date(b, ymd)?;
Ok(())
}
}
macro_rules! date_format {Как видите, ничего сложного тут нет 😈. Проверим, как это ведёт себя:
() => { Nil };
($name:ident $($tt:tt)*) => { Cons($name, date_format!($($tt)*)) };
($lit:literal $($tt:tt)*) => { Cons($lit, date_format!($($tt)*)) };
}
let d = date!(2020-02-18);К сожалению, писать имена вплотную после литералов нельзя, потому что это синтаксическая ошибка. В остальном — оно работает!
let format = date_format!(DAY" " MONTH_FULL", " YEAR);
assert_eq!(d.format_as(format), "18 February, 2020");
Ymd
, заставляет вызывать Date::as_ymd
даже в том случае, если используется только одно из значений месяц или день — а переход на Ymd
был совершён именно по соображениям производительности! У меня есть идеи, как можно решить этот недостаток, но... Это потребует несколько более тяжёлой ти́повой наркомании, так что это материал для следующей статьи.