从 &str [Rust] 解析日期的 Rustacean 方法

Rustacean way to parse a date from &str [Rust]

我有这个功能,但感觉是重复代码。我想知道这里是否有人分享如何让它变得更多 rustacean。我仍在学习 Rust,我认为这可能是一个很好的例子来分享。

fn check_and_transform_dates(start_date: &str, end_date: &str) -> (i64, i64) {
    let message: String = format!(
        "Data could not be downloaded ❌, please make sure your dates
    are in the following format YYYY-MM-DD
    (ie. 2020-01-01), your dates are Start Date: {}, End Date: {}",
        &start_date, &end_date,
    );
    let start_date_parsed: i64 = Utc
        .ymd(
            FromStr::from_str(start_date.split('-').collect::<Vec<&str>>()[0]).unwrap_or_else(
                |_| {
                    eprintln!("{}", &message);
                    process::exit(1);
                },
            ),
            FromStr::from_str(start_date.split('-').collect::<Vec<&str>>()[1]).unwrap_or_else(
                |_| {
                    eprintln!("{}", &message);
                    process::exit(1);
                },
            ),
            FromStr::from_str(start_date.split('-').collect::<Vec<&str>>()[2]).unwrap_or_else(
                |_| {
                    eprintln!("{}", &message);
                    process::exit(1);
                },
            ),
        )
        .and_hms_milli(0, 0, 1, 0)
        .timestamp_millis()
        .clamp(
            Utc.ymd(2016, 1, 1)
                .and_hms_milli(0, 0, 0, 0)
                .timestamp_millis(),
            Utc::now().timestamp_millis(),
        );
    let end_date_parsed: i64 = Utc
        .ymd(
            FromStr::from_str(end_date.split('-').collect::<Vec<&str>>()[0]).unwrap_or_else(|_| {
                eprintln!("{}", &message);
                process::exit(1);
            }),
            FromStr::from_str(end_date.split('-').collect::<Vec<&str>>()[1]).unwrap_or_else(|_| {
                eprintln!("{}", &message);
                process::exit(1);
            }),
            FromStr::from_str(end_date.split('-').collect::<Vec<&str>>()[2]).unwrap_or_else(|_| {
                eprintln!("{}", &message);
                process::exit(1);
            }),
        )
        .and_hms_milli(0, 0, 2, 0)
        .timestamp_millis()
        .clamp(
            Utc.ymd(2016, 1, 1)
                .and_hms_milli(0, 0, 0, 0)
                .timestamp_millis(),
            Utc::now().timestamp_millis(),
        );

    (start_date_parsed, end_date_parsed)

主要是去掉传递给Utc.ymd的三个参数,因为他们做的是一样的,只是使用了不同的索引,他们正在解析诸如“2021-01-01”之类的日期并以毫秒为单位返回它并且将它夹在地板和天花板上。

我们可以将“不要重新发明轮子”称为 Rustacean 方式吗?大概..

Playground

您可以使用 NaiveDate::parse_from_str,有很多格式选项。 然后,您可以使用 Utc::from_utc_dateUtc::from_local_date 获取 Date 并稍后像处理

一样处理它

这将代码缩减为:

use chrono::*;

fn check_and_transform_dates(start_date: &str, end_date: &str) -> (i64, i64) {
    let message: String = format!(
        "Data could not be downloaded ❌, please make sure your dates
    are in the following format YYYY-MM-DD
    (ie. 2020-01-01), your dates are Start Date: {}, End Date: {}",
        &start_date, &end_date,
    );
    let start_date = NaiveDate::parse_from_str(start_date, "%Y-%m-%d")
        .expect(&message);
    let start_date_parsed: i64 = Utc.from_utc_date(&start_date)
        .and_hms_milli(0, 0, 1, 0)
        .timestamp_millis()
        .clamp(
            Utc.ymd(2016, 1, 1)
                .and_hms_milli(0, 0, 0, 0)
                .timestamp_millis(),
            Utc::now().timestamp_millis(),
        );
    let end_date = NaiveDate::parse_from_str(end_date, "%Y-%m-%d")
        .expect(&message);
    let end_date_parsed: i64 = Utc.from_utc_date(&end_date)
        .and_hms_milli(0, 0, 2, 0)
        .timestamp_millis()
        .clamp(
            Utc.ymd(2016, 1, 1)
                .and_hms_milli(0, 0, 0, 0)
                .timestamp_millis(),
            Utc::now().timestamp_millis(),
        );

    (start_date_parsed, end_date_parsed)
}

这就是代码设计问题的来源,它是基于意见的。你看 - 你重复相同的代码两次来处理开始和结束日期 - 理想情况下,这段代码应该成为一个单独的函数。

use chrono::*;

fn parse_and_transform_date(date_str: &str) -> Result<i64, format::ParseError> {
    let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")?;
    let date_parsed: i64 = Utc.from_utc_date(&date)
        .and_hms_milli(0, 0, 1, 0)
        .timestamp_millis()
        .clamp(
            Utc.ymd(2016, 1, 1)
                .and_hms_milli(0, 0, 0, 0)
                .timestamp_millis(),
            Utc::now().timestamp_millis(),
        );
    Ok(date_parsed)
}
fn check_and_transform_dates(start_date: &str, end_date: &str) -> (i64, i64) {
    let start_date_result = parse_and_transform_date(start_date);
    let end_date_result = parse_and_transform_date(end_date);
    if let (Ok(start_date), Ok(end_date)) = (start_date_result, end_date_result) {
        return (start_date, end_date); // success
    }
    panic!(
        "Data could not be downloaded ❌, please make sure your dates
    are in the following format YYYY-MM-DD
    (ie. 2020-01-01), your dates are Start Date: {}, End Date: {}",
        &start_date, &end_date,
    );
}

最后也让大家考虑几个问题:

  1. 用这样的错误信息中止你的整个程序不是一个好主意,我建议 check_and_transform_dates 代替 return a Result 然后考虑这个结果,你的调用代码应正确处理这种情况。
  2. check_and_transform_dates 也应该做额外的检查,例如检查 end_date 不在 start_date 之前,等等

修复示例:

use chrono::*;

fn parse_and_transform_date(date_str: &str) -> Result<i64, format::ParseError> {
    let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")?;
    let date_parsed: i64 = Utc.from_utc_date(&date)
        .and_hms_milli(0, 0, 1, 0)
        .timestamp_millis()
        .clamp(
            Utc.ymd(2016, 1, 1)
                .and_hms_milli(0, 0, 0, 0)
                .timestamp_millis(),
            Utc::now().timestamp_millis(),
        );
    Ok(date_parsed)
}

fn check_and_transform_dates(start_date: &str, end_date: &str) -> Option<(i64, i64)> {
    let start_date_result = parse_and_transform_date(start_date);
    let end_date_result = parse_and_transform_date(end_date);
    match (start_date_result, end_date_result) {
        (Ok(s), Ok(e)) if (s <= e) => Some((s, e)),
        _ => None
    }
}

fn main() {
    println!("{:?}", check_and_transform_dates("2020-01-02", "2020-01-03")
        .expect("Data could not be downloaded"));
}

我要进行重构

fn check_and_transform_dates(start_date: &str, end_date: &str) -> (i64, i64) {
    let message: String = format!(
        "Data could not be downloaded ❌, please make sure your dates
    are in the following format YYYY-MM-DD
    (ie. 2020-01-01), your dates are Start Date: {}, End Date: {}",
        &start_date, &end_date,
    );

    let earliest: NaiveDate = NaiveDate::from_ymd(2016, 1, 1);
    let today: NaiveDate = Utc::today().naive_utc();

    let parse_date = |date: &str| -> NaiveDate {
        let date: NaiveDate = NaiveDate::parse_from_str(date, "%F").unwrap_or_else(|_| {
            eprintln!("{}", &message);
            process::exit(1);
        });
        if date < earliest {
            earliest
        } else if date > today {
            today
        } else {
            date
        }
    };

    (
        parse_date(start_date).and_hms(0, 0, 1).timestamp() * 1000,
        parse_date(end_date).and_hms(0, 0, 2).timestamp() * 1000,
    )
}

做一个检查夹紧的功能
fn check_and_clamp_date() 需要 start_dateend_date

extern crate chrono;
use chrono::{Duration, NaiveDate, Utc};

static FMT: &str = "%Y-%m-%d";

fn check_and_clamp_date(date: &str, ceil_date: &str, floor_date: &str) -> Result<i64, String> {
    let ceil = NaiveDate::parse_from_str(ceil_date, FMT).expect("start corrupt");
    let floor = NaiveDate::parse_from_str(floor_date, FMT).expect("end corrupt");
    let r = NaiveDate::parse_from_str(date, FMT);
    match r {
        Err(_) => Err("invalid date format".to_string()),
        Ok(d) => {
            let delta: Duration = d.clamp(ceil, floor).signed_duration_since(NaiveDate::from_ymd(1970, 1, 1));
            Ok(delta.num_nanoseconds().unwrap() / 1000000)
        },
    }
}

fn check_and_transform_dates(start_date: &str, end_date: &str) -> (i64, i64) {
    let message: String = format!(
        "Data could not be downloaded ❌, please make sure your dates
    are in the following format YYYY-MM-DD
    (ie. 2020-01-01), your dates are Start Date: {}, End Date: {}",
        &start_date, &end_date,
    );
    let now = Utc::now().format(FMT).to_string();
    let start = check_and_clamp_date(start_date, "2016-01-01", &now).expect(&message) + 1000;
    let end = check_and_clamp_date(end_date, "2016-01-01", &now).expect(&message) + 2000;
    (start, end)
}