Datum und Uhrzeit in Rust mit chrono
Programme haben ständig mit Zeit zu tun. „Wie lange hat das gedauert?“ „Wann ist die Rechnung fällig?“ „Wie viele Tage sind es noch bis zur Reise?“ Rust teilt diese Fragen in zwei Arten auf.
Das Modul std::time aus der Standardbibliothek ist eine Stoppuhr: Es misst, wie viel Zeit vergeht. Von Kalendern, Monaten oder Wochentagen weiß es nichts. Für Kalenderarbeit benutzt fast jedes Rust-Projekt das Crate chrono. Diese Seite beginnt mit der Stoppuhr und geht dann zum Kalender über.
Vergangene Zeit messen mit Instant
Abschnitt betitelt „Vergangene Zeit messen mit Instant“Instant::now() liest die Uhr des Computers ab. Später sagt dir .elapsed(), wie viel Zeit seit diesem Moment vergangen ist, und zwar als Duration:
use std::time::{Duration, Instant};
fn main() { let start = Instant::now();
let mut total: u64 = 0; for i in 0..1_000_000 { total += i; }
let elapsed = start.elapsed(); println!("Summe = {total}"); println!("Dauer: {:?}", elapsed); println!("Dauer: {} ms", elapsed.as_millis()); println!("unter 5 Sekunden? {}", elapsed < Duration::from_secs(5));}Die Ausgabe zeigen wir hier absichtlich nicht: Die Zeit ist auf jedem Computer und bei jedem Lauf anders. Du siehst etwa Dauer: 6.862793ms. Klick ruhig zweimal auf den Button und vergleiche.
Ein Instant taugt nur zum Messen. Du kannst ihn nicht als Datum ausgeben, und er läuft nur vorwärts. Genau das willst du, wenn du Code stoppst, denn es macht ihm nichts aus, wenn jemand die Systemuhr verstellt.
Zeitspannen mit Duration
Abschnitt betitelt „Zeitspannen mit Duration“Eine Duration ist eine Zeitspanne, zum Beispiel „90 Sekunden“. Du erzeugst sie aus Sekunden oder Millisekunden, addierst Zeitspannen und vergleichst sie:
use std::time::Duration;
fn main() { let short = Duration::from_millis(1500); let long = Duration::from_secs(90);
println!("{:?}", short); println!("{:?}", long); println!("{} ms", short.as_millis()); println!("{:.1} s", short.as_secs_f64());
let total = short + long; println!("gesamt: {:?}", total); println!("{} min {} s", long.as_secs() / 60, long.as_secs() % 60); println!("{}", short < long);}Ausgabe:
1.5s90s1500 ms1.5 sgesamt: 91.5s1 min 30 strueDuration begegnet dir an vielen Stellen: std::thread::sleep(Duration::from_millis(100)) pausiert einen Thread, und auch Timeouts in Netzwerkbibliotheken erwarten eine Duration. Beachte: Rust schreibt Kommazahlen immer mit Punkt, also 1.5s.
Die Uhrzeit des Systems mit SystemTime
Abschnitt betitelt „Die Uhrzeit des Systems mit SystemTime“SystemTime ist die Zeit auf der Uhr des Computers – die, die du in der Ecke deines Bildschirms siehst. Meistens fragst du damit, wie viel Zeit seit der Unix-Epoche vergangen ist, also seit dem 1. Januar 1970 (UTC). Diese Zahl heißt Unix-Zeitstempel:
use std::time::{SystemTime, UNIX_EPOCH};
fn main() { let now = SystemTime::now(); let since_1970 = now .duration_since(UNIX_EPOCH) .expect("die Uhr steht vor 1970"); println!("Sekunden seit dem 1. Januar 1970: {}", since_1970.as_secs());}Die Zahl hängt davon ab, wann du das Programm ausführst, deshalb gibt es auch hier keinen Ausgabeblock. Warum gibt duration_since ein Result zurück? Weil die Systemuhr falsch gehen oder zurückgestellt werden kann. Auch deshalb nimmst du zum Messen von Code Instant und nicht SystemTime.
Viel mehr kann std::time nicht. Was „März“ oder „Freitag“ bedeutet, weiß es nicht. Dafür brauchst du chrono.
chrono hinzufügen
Abschnitt betitelt „chrono hinzufügen“Füge das Crate zu deinem Projekt hinzu (siehe Cargo und Crates, falls das neu für dich ist):
cargo add chrono[dependencies]chrono = "0.4"Der Rust Playground hat chrono auch dabei, also funktioniert jeder Button unten. chrono hat zwei Familien von Typen:
- Naive Typen (
NaiveDate,NaiveTime,NaiveDateTime) sind Datums- und Zeitangaben ohne Zeitzone. Etwa „14. März 2025“ oder „09:30“, so wie sie in einem Kalender stehen. Der meiste Alltagscode benutzt diese Typen. DateTime<Tz>ist ein Zeitpunkt mit Zeitzone, zum BeispielDateTime<Utc>oderDateTime<Local>.
Daten aus festen Werten erstellen
Abschnitt betitelt „Daten aus festen Werten erstellen“NaiveDate::from_ymd_opt(jahr, monat, tag) baut ein Datum. Die Funktion gibt eine Option zurück, denn nicht jede Kombination ist ein echtes Datum:
use chrono::{Datelike, NaiveDate, NaiveTime};
fn main() { let date = NaiveDate::from_ymd_opt(2025, 3, 14).unwrap(); println!("{date}"); println!("Jahr {}, Monat {}, Tag {}", date.year(), date.month(), date.day());
let time = NaiveTime::from_hms_opt(9, 30, 0).unwrap(); let meeting = date.and_time(time); println!("{meeting}");
let other = date.and_hms_opt(17, 45, 10).unwrap(); println!("{other}");
let impossible = NaiveDate::from_ymd_opt(2025, 2, 30); println!("{:?}", impossible);}Ausgabe:
2025-03-14Jahr 2025, Monat 3, Tag 142025-03-14 09:30:002025-03-14 17:45:10NoneEinen 30. Februar gibt es nicht, also bekommst du None statt eines kaputten Datums. Bei festen Werten, die du selbst eingetippt hast, ist .unwrap() in Ordnung. Bei Werten von Benutzern behandelst du das None.
Die Zeile use chrono::Datelike holt die Methoden .year(), .month() und .day() in den Scope. (Timelike macht dasselbe für .hour() und .minute().)
Das aktuelle Datum und die aktuelle Uhrzeit
Abschnitt betitelt „Das aktuelle Datum und die aktuelle Uhrzeit“Utc::now() liefert den aktuellen Zeitpunkt in UTC. Local::now() liefert ihn in der Zeitzone des Computers:
use chrono::{Local, Utc};
fn main() { let utc_now = Utc::now(); let local_now = Local::now();
println!("UTC: {}", utc_now); println!("Lokal: {}", local_now); println!("Heute: {}", local_now.format("%Y-%m-%d"));
let today = local_now.date_naive(); println!("Als NaiveDate: {today}");}Die Ausgabe ist das heutige Datum und ändert sich also jeden Tag. Die Server des Playgrounds laufen in UTC, deshalb zeigen dort beide Zeilen dieselbe Uhrzeit. .date_naive() wirft Zeitzone und Uhrzeit weg und lässt ein einfaches NaiveDate übrig. Eine gute Angewohnheit: Hol dir „jetzt“ einmal am Rand deines Programms und reich das Datum dann an deine Funktionen weiter. So bleiben sie leicht testbar, wie du im Fristen-Beispiel sehen wirst.
Daten formatieren
Abschnitt betitelt „Daten formatieren“.format() nimmt ein Muster. Jeder %-Code wird durch einen Teil des Datums ersetzt. %Y ist das Jahr, %m die Monatszahl, %d der Tag:
use chrono::NaiveDate;
fn main() { let date = NaiveDate::from_ymd_opt(2025, 3, 14).unwrap(); let dt = date.and_hms_opt(9, 5, 0).unwrap();
println!("{}", date.format("%Y-%m-%d")); println!("{}", date.format("%d.%m.%Y")); println!("{}", date.format("%A, %B %-d, %Y")); println!("{}", date.format("%a %d %b")); println!("{}", dt.format("%H:%M")); println!("{}", dt.format("%-I:%M %p"));
let file_name = date.format("bericht-%Y%m%d.txt").to_string(); println!("{file_name}");}Ausgabe:
2025-03-1414.03.2025Friday, March 14, 2025Fri 14 Mar09:059:05 AMbericht-20250314.txtDie Codes, die du am häufigsten brauchst: %Y Jahr, %m Monat (01–12), %d Tag (01–31), %H Stunde (00–23), %M Minute, %S Sekunde, %A Name des Wochentags, %B Name des Monats. Ein - nach dem % entfernt die führende Null, %-d gibt also 5 statt 05 aus. .format() liefert etwas, das du ausgeben kannst. Wenn du einen String brauchst, ruf .to_string() auf.
Vielleicht wundert dich das „Friday“: chrono schreibt Wochentags- und Monatsnamen standardmäßig auf Englisch. Für das deutsche Datumsformat mit Zahlen wie 14.03.2025 reicht das Muster %d.%m.%Y. Wenn du deutsche Namen wie „Freitag“ brauchst, hat chrono dafür das optionale Feature unstable-locales (siehe die chrono-Dokumentation unter „Weiterlesen“).
Text in ein Datum umwandeln (Parsen)
Abschnitt betitelt „Text in ein Datum umwandeln (Parsen)“NaiveDate::parse_from_str macht das Gegenteil: Es liest Text mit demselben %-Muster ein. Es gibt ein Result zurück, denn der Text passt vielleicht nicht zum Muster:
use chrono::{NaiveDate, NaiveDateTime};
fn main() { let a = NaiveDate::parse_from_str("2025-03-14", "%Y-%m-%d"); println!("{:?}", a);
let b = NaiveDate::parse_from_str("14.03.2025", "%d.%m.%Y"); println!("{:?}", b);
let c: Result<NaiveDate, _> = "2025-03-14".parse(); println!("{:?}", c);
let d = NaiveDateTime::parse_from_str("2025-03-14 09:30", "%Y-%m-%d %H:%M"); println!("{:?}", d);
for text in ["2025-13-01", "March 14", "2025-03"] { match NaiveDate::parse_from_str(text, "%Y-%m-%d") { Ok(date) => println!("Gelesen: {date}"), Err(e) => println!("Konnte {text:?} nicht lesen: {e}"), } }}Ausgabe:
Ok(2025-03-14)Ok(2025-03-14)Ok(2025-03-14)Ok(2025-03-14T09:30:00)Konnte "2025-13-01" nicht lesen: input is out of rangeKonnte "March 14" nicht lesen: input contains invalid charactersKonnte "2025-03" nicht lesen: premature end of inputFür die Standardform YYYY-MM-DD kannst du auch einfach .parse() aufrufen. Einen Monat 13 gibt es nicht, deshalb ist das erste schlechte Beispiel „out of range“. Die Fehlermeldungen kommen von chrono und sind auf Englisch: „außerhalb des gültigen Bereichs“, „enthält ungültige Zeichen“ und „Eingabe endet zu früh“. Sie sind kurz, aber klar genug, um sie einem Benutzer zu zeigen.
Mit Daten rechnen
Abschnitt betitelt „Mit Daten rechnen“Tage von Hand zu einem Datum zu addieren ist erstaunlich schwierig: Monate sind unterschiedlich lang, und es gibt Schaltjahre. chrono nimmt dir das ab. Benutze checked_add_days mit Days, checked_add_months mit Months, oder addiere ein TimeDelta:
use chrono::{Days, Months, NaiveDate, TimeDelta};
fn main() { let start = NaiveDate::from_ymd_opt(2025, 1, 31).unwrap();
let plus_30 = start.checked_add_days(Days::new(30)).unwrap(); println!("30 Tage später: {plus_30}");
let minus_7 = start.checked_sub_days(Days::new(7)).unwrap(); println!("7 Tage früher: {minus_7}");
let next_month = start.checked_add_months(Months::new(1)).unwrap(); println!("1 Monat später: {next_month}");
let week_later = start + TimeDelta::weeks(1); println!("1 Woche später: {week_later}");
let dt = start.and_hms_opt(23, 30, 0).unwrap(); let later = dt + TimeDelta::hours(2); println!("2 Stunden später: {later}");}Ausgabe:
30 Tage später: 2025-03-027 Tage früher: 2025-01-241 Monat später: 2025-02-281 Woche später: 2025-02-072 Stunden später: 2025-02-01 01:30:00Schau dir „1 Monat später“ an: 31. Januar plus ein Monat ist der 28. Februar, denn einen 31. Februar gibt es nicht. chrono nimmt dann den letzten gültigen Tag. Die checked_-Methoden geben eine Option zurück. Die ist nur dann None, wenn das Ergebnis außerhalb von chronos (sehr großem) Jahresbereich liegt. Und 2 Stunden nach 23:30 springt das Datum korrekt auf den nächsten Tag.
Wochentage und der Abstand zwischen zwei Daten
Abschnitt betitelt „Wochentage und der Abstand zwischen zwei Daten“.weekday() (aus Datelike) sagt dir den Wochentag. Wenn du ein Datum von einem anderen abziehst, bekommst du ein TimeDelta, und .num_days() macht daraus eine Zahl:
use chrono::{Datelike, NaiveDate, Weekday};
fn main() { let date = NaiveDate::from_ymd_opt(2025, 3, 15).unwrap(); let day = date.weekday(); println!("{date} ist ein {day} ({})", date.format("%A"));
let weekend = matches!(day, Weekday::Sat | Weekday::Sun); println!("Wochenende? {weekend}");
let start = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(); let end = NaiveDate::from_ymd_opt(2025, 12, 25).unwrap(); let gap = end - start; println!("{} Tage oder {} volle Wochen", gap.num_days(), gap.num_weeks()); println!("rückwärts: {} Tage", (start - end).num_days());
let check_in = start.and_hms_opt(14, 0, 0).unwrap(); let check_out = NaiveDate::from_ymd_opt(2025, 1, 4).unwrap().and_hms_opt(11, 0, 0).unwrap(); println!("Hotelaufenthalt: {} Stunden", (check_out - check_in).num_hours());}Ausgabe:
2025-03-15 ist ein Sat (Saturday)Wochenende? true358 Tage oder 51 volle Wochenrückwärts: -358 TageHotelaufenthalt: 69 StundenWeekday ist ein Enum mit den Varianten Mon bis Sun, du kannst also mit match darauf prüfen. Beachte, dass der Abstand negativ sein kann, wenn das zweite Datum später liegt. Das ist praktisch: Eine negative Zahl verbleibender Tage bedeutet „überfällig“.
Beispiel: Tage bis zu einer Frist
Abschnitt betitelt „Beispiel: Tage bis zu einer Frist“Setzen wir alles zusammen. Die Funktion bekommt „heute“ als Parameter, statt intern Local::now() aufzurufen. So ist die Ausgabe unten immer gleich, und die Funktion lässt sich leicht testen. In einem echten Programm würdest du Local::now().date_naive() übergeben.
Die Texte in diesem Beispiel bleiben auf Englisch. {:<20} richtet die Spalten aus, und chrono gibt die Monats- und Wochentagsnamen ohnehin auf Englisch aus. So bleibt die Tabelle einheitlich.
use chrono::NaiveDate;
fn days_left(today: NaiveDate, deadline: NaiveDate) -> String { let days = (deadline - today).num_days(); match days { 0 => String::from("due today"), 1 => String::from("due tomorrow"), d if d > 1 => format!("{d} days left"), -1 => String::from("1 day overdue"), d => format!("{} days overdue", -d), }}
fn main() { let today = NaiveDate::from_ymd_opt(2025, 6, 10).unwrap();
let tasks = [ ("Pay rent", "2025-06-10"), ("Send report", "2025-06-11"), ("Renew passport", "2025-07-01"), ("Return library book", "2025-06-09"), ("Dentist bill", "2025-06-03"), ];
println!("Today is {}", today.format("%A, %-d %B %Y")); for (name, date_text) in tasks { let deadline = NaiveDate::parse_from_str(date_text, "%Y-%m-%d").unwrap(); println!("{:<20} {} {}", name, deadline.format("%d %b"), days_left(today, deadline)); }}Ausgabe:
Today is Tuesday, 10 June 2025Pay rent 10 Jun due todaySend report 11 Jun due tomorrowRenew passport 01 Jul 21 days leftReturn library book 09 Jun 1 day overdueDentist bill 03 Jun 7 days overdueÄndere today auf ein anderes Datum und führ das Programm noch einmal aus. {:<20} füllt den Namen auf 20 Zeichen auf, damit die Spalten untereinanderstehen (siehe Ausgabe).
chrono ist nicht die einzige Wahl. Das Crate time ist eine kleinere, beliebte Alternative, und das neuere Crate jiff gibt sich besonders viel Mühe mit Zeitzonen und Sommerzeit. Wir benutzen hier chrono, weil es am weitesten verbreitet ist und im Playground zur Verfügung steht.
Häufige Fehler
Abschnitt betitelt „Häufige Fehler“use chrono::Datelike vergessen. Ohne diese Zeile funktionieren .year(), .month() und .weekday() nicht. Der Fehler sagt, die Methode sei „private“ (privat). Das verwirrt, aber die help-Zeile nennt dir die echte Lösung: Der Trait Datelike stellt year bereit, ist aber nicht im Scope – importiere ihn:
error[E0624]: method `year` is private --> src/main.rs:5:25 |5 | println!("{}", date.year()); | ^^^^ private method | = help: items from traits can only be used if the trait is in scopehelp: trait `Datelike` which provides `year` is implemented but not in scope; perhaps you want to import it |1 + use chrono::Datelike; |%m und %M verwechseln. %m ist der Monat, %M die Minute. Bei Datum mit Uhrzeit gibt die falsche Variante keinen Fehler. Sie gibt still und leise die Minuten aus:
use chrono::NaiveDate;
fn main() { let meeting = NaiveDate::from_ymd_opt(2025, 3, 14).unwrap().and_hms_opt(9, 5, 0).unwrap(); println!("{}", meeting.format("%Y-%M-%d")); // falsch: Minuten println!("{}", meeting.format("%Y-%m-%d")); // richtig: Monat}Ausgabe:
2025-05-142025-03-14Code mit SystemTime stoppen. Die Systemuhr kann springen (zum Beispiel, wenn sie sich mit dem Internet abgleicht). Nimm zum Messen Instant und SystemTime oder chrono nur, wenn du das tatsächliche Datum brauchst.
Local::now() tief in deiner Logik aufrufen. Dann ändern sich die Ergebnisse jeden Tag, und Tests werden schwer. Übergib das Datum als Parameter, so wie days_left es macht.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“std::time::Instantmisst vergangene Zeit,Durationist eine Zeitspanne, undSystemTimeist die Uhr des Systems (gut für Unix-Zeitstempel).- Für Kalender fügst du
chronohinzu. NimmNaiveDate/NaiveDateTimefür Daten ohne Zeitzone undUtc::now()/Local::now()für den aktuellen Zeitpunkt. from_ymd_optgibt eineOptionzurück undparse_from_streinResult, denn nicht jede Eingabe ist ein echtes Datum..format("%Y-%m-%d")macht aus einem Datum Text mit%-Codes. Merke dir:%mist der Monat,%Mdie Minute.- Zeit addierst du mit
checked_add_days,checked_add_monthsoder+ TimeDelta. Zieh zwei Daten voneinander ab, um einTimeDeltazu bekommen, und ruf dann.num_days()auf. - Reich „heute“ an deine Funktionen weiter, statt in ihnen die Uhr abzulesen.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Welchen Typ benutzt du, um zu messen, wie lange ein Stück Code braucht?
2. Was gibt NaiveDate::from_ymd_opt(2025, 2, 30) zurück?
3. Was bedeutet %M in einem Format-String von chrono?
4. Du ziehst zwei NaiveDates voneinander ab: deadline - today. Wie bekommst du die Anzahl der Tage?
5. Was ergibt der 31. Januar 2025 plus Months::new(1) in chrono?
Punkte: 0 / 5
Weiterlesen
Abschnitt betitelt „Weiterlesen“- std::time – Doku der Standardbibliothek (auf Englisch):
Instant,DurationundSystemTimemit weiteren Beispielen. - Dokumentation von chrono – docs.rs (auf Englisch): die vollständige Anleitung zu jedem Typ und jeder Methode von chrono.
- Format-Codes von chrono – docs.rs (auf Englisch): die komplette Liste der
%-Codes fürformatundparse_from_str. - Date and Time – Rust Cookbook (auf Englisch): kurze Rezepte für häufige Aufgaben mit Datum und Uhrzeit.
- Dokumentation von jiff – docs.rs (auf Englisch): ein neueres Crate für Datum und Uhrzeit mit starker Unterstützung für Zeitzonen, falls du vergleichen willst.