Question to chrono and iterate over months

I try to port an small Java Tool to rust and have some porting issues like that one.

I would like to iterate from local_minus_six_months month to local_now and create an arry/list similar to the java code below.

I was able to calculate the minus 7 month from now on but haven't seen any chrono function which allows to iterate from an startpoint to the destination similar to datesUntil. Is there something similar or do I need to solve this in another way?

Could be jiff a better aproach?

Rust:

use std::process::exit;
use chrono::{Local, Months};

fn main() {
    println!("Hello, world!");
    let local_now = Local::now();

    let local_minus_n_months = match local_now.checked_sub_months(Months::new(7)) {
        None => {
            println!("Error at checked_sub_months");
            exit(-1);
        },
        Some(new_months) => new_months,
    };
    println!("Now {}",local_now.format("%Y-%m"));
    println!("-6M {}",local_minus_n_months.format("%Y-%m"));
}

Java:
The code below creates a String toGlobFiles which holds the direcory glob in that format *{YYYY-MM,YYYY-MM,YYYY-MM}*.{gz,log}


        StringBuffer toGlobFiles = new StringBuffer("*{");
		LocalDate ldStart = LocalDate.now().minusMonths(6);

		// Calculate the Date range
		List<LocalDate> dates = ldStart // Determine your beginning `LocalDate` object.
				.datesUntil(            // Generate stream of `LocalDate` objects.
		           ldStart.plusMonths(7), 
                   Period.ofMonths(1) // Calculate your ending date, and ask for a stream of
																	// dates till then.
				) // Returns the stream.
				.collect(Collectors.toList()); // Collect your resulting dates in a `List`.

		for (LocalDate localDate : dates) {
			logger.info("{}", localDate.format(DateTimeFormatter.ofPattern("YYYY-MM,")));
			toGlobFiles.append(localDate.format(DateTimeFormatter.ofPattern("YYYY-MM,")));

       // Remove last ',' from the loop above
       toGlobFiles.setLength(toGlobFiles.length() - 1);
       toGlobFiles.append("}*.{gz,log}");

       logger.info("toGlobFiles {}", toGlobFiles);
		}

chrono doesn't have an equivalence to java datesUntil, but chrono does provide implementations of PartialEq and PartialOrd, so it's easy to write your own Iterator, here's an example using std::iter::from_fn():

EDIT:

the old code was incorrect for the day of month. see comment by @BurntSushi below. it should be fixed now.

jiff is a better fit for this task.

END of EDIT

fn months_between<Tz: TimeZone<Offset: Copy>>(
	start: DateTime<Tz>,
	end: DateTime<Tz>,
) -> impl Iterator<Item = DateTime<Tz>> {
	let mut elapsed_months = 0;
	std::iter::from_fn(move || {
		let next = start + Months::new(elapsed_months);
		if next < end {
			elapased_months += 1;
			Some(next)
		} else {
			None
		}
	})
}

here how to use it using your example:

Wow, thank you sooo much @nerditation for your help and solution. :folded_hands: :person_bowing:

One thing to be careful of with the above code snippet is if you need the day (and not just the month/year) and you start from the end of a 31-day month, you may get unexpected results:

Now 2025-10-31
-6M 2025-03-31
* 2025-03-31
* 2025-04-30
* 2025-05-30
* 2025-06-30
* 2025-07-30
* 2025-08-30
* 2025-09-30
* 2025-10-30

The Jiff approach is considerably simpler and won't suffer from the problem above:

use jiff::{ToSpan, Zoned};

fn main() -> anyhow::Result<()> {
    let start: Zoned =
        "2025-10-31T17:30:00-04:00[America/New_York]".parse()?;
    let start_minus_n_months = start.checked_sub(7.months())?;
    println!("Now {}", start.strftime("%Y-%m-%d"));
    println!("-6M {}", start_minus_n_months.strftime("%Y-%m-%d"));

    let it = start_minus_n_months
        .datetime()
        .series(1.month())
        .filter_map(|dt| dt.to_zoned(start.time_zone().clone()).ok())
        .take_while(|zdt| zdt <= start);
    for zdt in it {
        println!("* {}", zdt.strftime("%Y-%m-%d"));
    }

    Ok(())
}

Has this output:

Now 2025-10-31
-6M 2025-03-31
* 2025-03-31
* 2025-04-30
* 2025-05-31
* 2025-06-30
* 2025-07-31
* 2025-08-31
* 2025-09-30
* 2025-10-31

In a future release of Jiff, I plan to add Zoned::series, so you'll be able to make this even simpler:

use jiff::{ToSpan, Zoned};

fn main() -> anyhow::Result<()> {
    let start: Zoned =
        "2025-10-31T17:30:00-04:00[America/New_York]".parse()?;
    let start_minus = start.checked_sub(7.months())?;
    println!("Now {}", start.strftime("%Y-%m-%d"));
    println!("-6M {}", start_minus.strftime("%Y-%m-%d"));

    for zdt in start_minus.series(1.month()).take_while(|x| x <= start) {
        println!("* {}", zdt.strftime("%Y-%m-%d"));
    }

    Ok(())
}
4 Likes

That's now the full code for the file glob, for the archive.

I have add it to the playground :slight_smile:

use jiff::{ToSpan, Zoned, fmt::strtime};
use std::{process::exit, str::FromStr};

fn main() {
    println!("Hello, world!");

    let mut to_glob_files = match String::from_str("*{") {
        Ok(new_str) => new_str,
        Err(e) => {
            println!("Error at String::from_str error: {:#?}", e.to_string());
            exit(-1);
        }
    };
    let start: Zoned = Zoned::now();
    let start_minus_n_months = match start.checked_sub(7.months()) {
        Ok(new_months) => new_months,
        Err(e_mon) => {
            println!(
                "Error at checked_sub_months error: {:#?}",
                e_mon.to_string()
            );
            exit(-2);
        }
    };

    let it = start_minus_n_months
        .datetime()
        .series(1.month())
        .filter_map(|dt| dt.to_zoned(start.time_zone().clone()).ok())
        .take_while(|zdt| zdt <= start);

    for zdt in it {
        let temp = match strtime::format("%Y-%m,", &zdt) {
            Ok(new_temp) => new_temp,
            Err(e_format) => {
                println!(
                    "Error at strtime::format error: {:#?}",
                    e_format.to_string()
                );
                exit(-2);
            }
        };
        to_glob_files.push_str(&temp);
//        println!("* {}", zdt.strftime("%Y-%m"));
    }

    // println!("capa: {}", to_glob_files.capacity());
    // println!("len: {}", to_glob_files.len());

    // Remove last ',' from the loop above
    to_glob_files.truncate(to_glob_files.len() - 1);

    // println!("len after -1: {}", to_glob_files.len());

    to_glob_files.push_str("}*.{gz,log}");

    println!("to glob: '{}'", to_glob_files);
}

2 Likes

that's a good catch. I edited my previous post to (hopefully) fix it.

1 Like

thanks for sharing the result.

just a reminder, calling exit() directly will NOT run destructors. thus it is more desirable to do an early return from the main function instead of calling exit().

note, main doesn't need to return the unit type, e.g. you can return an Result from main, which would also simplify the error handling in this example. see the Termination trait for details.

the most easy approach is to use Result<(), Box<dyn Error>> as the return type of main, so you can easily propagate errors with the ? operator, and an error message is automatically printed when the return value is Err.

however, if the exit code (e.g. -1 or -2 in your example) is hard requirement by the application spec, or you need specific error message formats, you'll need to create a custom return type, because Result will map any error into the same exit code: the equivalence to EXIT_FAILURE in C.

3 Likes

Depending on how "quick and dirty" the program is, this may not matter, but... I only recommend returning a Result<_, _> from main if the error type has been specially tailored to have an end-user friendly Debug implementation (e.g. anyhow::Error), or if the error case in main can only happen due to a bug in the program. The Debug implementation for most error types is not end-user friendly, and that's what you get with Result's Termination implementation, sadly.[1]

That said, using anyhow::Result<_> can also simply error handling with decent results, so perhaps that's my real advice to the OP :slight_smile:.


I took my own gander at the code too.

    let mut to_glob_files = match String::from_str("*{") {

That never returns an error. I suggest:

    let mut to_glob_files = "*{".to_owned();

            println!(
                "Error at checked_sub_months error: {:#?}",
                e_mon.to_string()
            );

to_string creates a String using the error type's Display implementation. Then you print the Debug representation of the String (which prints the value wrapped in "..."). Then the temporary String is dropped. I would just[2]

            println!("Error at checked_sub_months error: \"{e_mon}\"");

Backing up a step, the error will only happen if the clock is in the extreme past...[3]

let start_minus_n_months = match start.checked_sub(7.months()) {

...so you could consider saturating_sub instead. Or a more targeted error.[4]


        let temp = match strtime::format("%Y-%m,", &zdt) {

I think this one can only return an error due to a bug in the program (e.g. invalid format), so you could consider just unwrapping or using expect.

You can also do this, though I recognize it's not as intuitive.

        BrokenDownTime::from(&zdt)
            .format("%Y-%m,", &mut to_glob_files)
            .expect("invalid date format");

To remove the last char of a String:

-    to_glob_files.truncate(to_glob_files.len() - 1);
+    let _ = to_glob_files.pop();

Here's a playground with the suggested changes.


  1. You can put your top-level code in another function and just "catch" errors in main for nicer display (or specific exit codes). ↩︎

  2. probably without the quotes even ↩︎

  3. More than it actually can be on Unix I think? I didn't dig deep enough to be completely sure though. ↩︎

  4. In the playground below I just used anyhow instead. ↩︎

2 Likes

Y'all can just use write!(to_glob_files, "{}", zdt.strftime("%Y-%m")). It'll avoid the intermediate alloc (albeit go through the formatting machinery). And @quinedot is correct that it will only panic if the format string is incorrect in some way (i.e., a bug, in which case, a panic is perfectly suitable).

2 Likes

Wow thanks to all for the very valuable feedback. For me as rust rookie are this very helpful insights.

I’m very happy to see here real people which are friendly, open and willing to support rookies. :person_bowing: :folded_hands:

Update for the archive: The current programm is now full at Help with glob/globset or generall get all files in an directory based on glob pattern - #6 by alex1