📆

chrono::NaiveDate::iter_daysでの注意

2022/12/04に公開

はじめに

この記事は Xbit Advent Calendar 2022 の4日目の記事です。
株式会社クロスビット | Xbit, Inc.
https://x-bit.co.jp/

主張

Rustで時間を扱うデファクトスタンダードなライブラリ(crate)として知られるchrono
において
NaiveDate::iter_days(&self)で得られる日付(NaiveDate)のIteratorに対して
take().rev()を呼び出した際に想定と異なる挙動をします。

https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDate.html#method.iter_days

iter_weeks(&self)についても同様です。

Code

[Playground]

main.rs
use chrono::NaiveDate;
use itertools::*;

fn main() {
    println!("{}", (25..).take(3).join(" "));          // 25 26 27
    // println!("{}", (25..).take(3).rev().join(" ")); // Unable to call rev()
    // println!("{}", (25..).rev().take(3).join(" ")); // Unable to call rev()
    println!();
    println!("{}", (25..99).take(3).join(" "));       // 25 26 27
    println!("{}", (25..99).take(3).rev().join(" ")); // 27 26 25
    println!("{}", (25..99).rev().take(3).join(" ")); // 98 97 96
    println!();

    let xmas = NaiveDate::from_ymd_opt(2022, 12, 25).unwrap();
    println!("{}", xmas.iter_days().take(3).join(" "));       // /25 /26 /27
    println!("{}", xmas.iter_days().take(3).rev().join(" ")); // ???
    println!("{}", xmas.iter_days().rev().take(3).join(" ")); // /25 /24 /23
}
stdout
25 26 27

25 26 27
27 26 25
98 97 96

2022-12-25 2022-12-26 2022-12-27
-258099-12-23
2022-12-25 2022-12-24 2022-12-23

対処法

  • take().rev()しない
  • 終わり日時を出してそこからiter_xxxs().rev().take()する
  • 一旦Vecなどにcollectする

原因について

根本的な原因は、iter_days()が無限iteratorであるにもかかわらず、

が(両方)実装されていることで、
TakeにDoubleEndedIteratorが実装される条件を満たしている
(=take().rev()と書ける)ことであるといえます。

std_iter_Take
impl<I> DoubleEndedIterator for Take<I>
where
    I: DoubleEndedIterator + ExactSizeIterator,

比較として他のIteratorを見ると、
iter_days()と似たIteratorといえるstd::ops::RangeFrom (start.. のようなrange)では
DoubleEndedIteratorもExactSizeIteratorも実装されていません。

実装されているIteratorの例としてはstd::ops::Range (start..end のようなrange)があり、
Range<整数型>などに対してDoubleEndedIteratorExactSizeIteratorが実装されています。

iter_days()にこれらのtraitが実装されている理由について考えられることとしては、
DoubleEndedIteratorについてはiter_days().rev()で逆順iteratorが得られるという仕様が"見た目ではわかりやすい"という利点がありますが、DoubleEndedIteratorの主旨には反しているように思えます。
ExactSizeIteratorについては良くわかりません。(前述のようにRangeFromには一切実装されていないことですし)

おわりに

株式会社クロスビットでは私達と一緒にスタッフスケジューリングの最適化システムなどの開発に携わるバックエンドエンジニア、フロントエンドエンジニア、あるいはフルスタックエンジニアを募集しています。
https://herp.careers/v1/xbit
https://x-bit.co.jp/


関連リンク

chronoのissue/PRを軽く調べて見つかった関係ありそうな項目を貼っておきます。

クロスビットテックブログ

Discussion