From: Dominik Csapak <d.csapak@proxmox.com>
To: yew-devel@lists.proxmox.com
Subject: [yew-devel] [PATCH yew-widget-toolkit 1/2] plain date: improve format parser
Date: Wed, 17 Dec 2025 11:30:10 +0100 [thread overview]
Message-ID: <20251217103017.1103164-1-d.csapak@proxmox.com> (raw)
by using more idiomatic rust code, and also fixes some bugs that are now
tested with additional test cases.
In particular this changes the parser from being purely index based to
using a mutable slice for proceeding, and an iterator + match for
handling the different format characters.
The new tests are for doing early error checking e.g. in case the format
string contains multiple variants of the same type, and requires valid
values for those.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/widget/form/date_field/plain_date.rs | 202 ++++++++++-------------
1 file changed, 91 insertions(+), 111 deletions(-)
diff --git a/src/widget/form/date_field/plain_date.rs b/src/widget/form/date_field/plain_date.rs
index 80c036a..e7d8a97 100644
--- a/src/widget/form/date_field/plain_date.rs
+++ b/src/widget/form/date_field/plain_date.rs
@@ -1,5 +1,5 @@
use js_sys::Date;
-use std::fmt;
+use std::{fmt, str::FromStr};
/// A date without time information (Year, Month, Day).
/// Months are 0-based (0 = January, 11 = December).
@@ -161,133 +161,102 @@ impl PlainDate {
/// - d: Day with leading zeros
/// - j: Day without leading zeros
/// Reference: https://www.php.net/manual/en/datetime.format.php
- pub fn from_format(input: &str, fmt: &str) -> Result<Self, String> {
+ pub fn from_format(mut input: &str, fmt: &str) -> Result<Self, String> {
// Implementation:
- let mut year = None;
- let mut month = None;
- let mut day = None;
+ let mut year: Option<i32> = None;
+ let mut month: Option<u32> = None;
+ let mut day: Option<u32> = None;
- // Convert to look-ahead friendly chars
- let fmt_chars: Vec<char> = fmt.chars().collect();
- let input_chars: Vec<char> = input.chars().collect();
-
- let mut f_idx = 0;
- let mut i_idx = 0;
+ fn parse_num<NUM: FromStr>(input: &str, err: String) -> Result<NUM, String> {
+ input.parse().map_err(|_| err)
+ }
- while f_idx < fmt_chars.len() {
- if i_idx >= input_chars.len() {
- return Err("Input too short".to_string());
+ fn find_num_length(input: &str) -> usize {
+ let mut chars = input.chars();
+ if let Some(c1) = chars.next() {
+ if c1.is_ascii_digit() {
+ if ('1'..='3').contains(&c1) {
+ if let Some(c2) = chars.next() {
+ if c2.is_ascii_digit() {
+ return 2;
+ }
+ }
+ }
+ return 1;
+ }
}
+ 0
+ }
- // Check tokens
- if fmt[f_idx..].starts_with("Y") {
- // PHP Y: 4 digit year
- if i_idx + 4 > input_chars.len() {
- return Err("Input ends before Year".into());
- }
- let y_str: String = input_chars[i_idx..i_idx + 4].iter().collect();
- year = y_str.parse::<i32>().ok();
- f_idx += 1;
- i_idx += 4;
- } else if fmt[f_idx..].starts_with("y") {
- // PHP y: 2 digit year
- if i_idx + 2 > input_chars.len() {
- return Err("Input ends before Year".into());
+ for f_char in fmt.chars() {
+ let took = match f_char {
+ 'Y' => {
+ // PHP Y: 4 digit year
+ if input.len() < 4 {
+ return Err("Input ends before Year".into());
+ }
+ year = Some(parse_num(&input[..4], "Invalid year".to_string())?);
+ 4
}
- let y_str: String = input_chars[i_idx..i_idx + 2].iter().collect();
- if let Ok(y2) = y_str.parse::<i32>() {
- // Pivot strategy: 2000-2099 defaults
- year = Some(2000 + y2);
+ 'y' => {
+ // PHP y: 2 digit year
+ if input.len() < 2 {
+ return Err("Input ends before Year".into());
+ }
+ let y: i32 = parse_num(&input[..2], "Invalid year".to_string())?;
+ year = Some(y + 2000);
+ 2
}
- f_idx += 1;
- i_idx += 2;
- } else if fmt[f_idx..].starts_with("m") {
- // PHP m: Month with leading zeros (01-12)
- if i_idx + 2 > input_chars.len() {
- return Err("Input ends before Month".into());
+ 'm' => {
+ // PHP m: Month with leading zeros (01-12)
+ if input.len() < 2 {
+ return Err("Input ends before Month".into());
+ }
+ month = Some(parse_num(&input[..2], "Invalid month".to_string())?);
+ 2
}
- let m_str: String = input_chars[i_idx..i_idx + 2].iter().collect();
- month = m_str.parse::<u32>().ok();
- f_idx += 1;
- i_idx += 2;
- } else if fmt[f_idx..].starts_with("d") {
- // PHP d: Day with leading zeros (01-31)
- if i_idx + 2 > input_chars.len() {
- return Err("Input ends before Day".into());
+ 'd' => {
+ // PHP d: Day with leading zeros (01-31)
+ if input.len() < 2 {
+ return Err("Input ends before Day".into());
+ }
+ day = Some(parse_num(&input[..2], "Invalid day".to_string())?);
+ 2
}
- let d_str: String = input_chars[i_idx..i_idx + 2].iter().collect();
- day = d_str.parse::<u32>().ok();
- f_idx += 1;
- i_idx += 2;
- } else if fmt[f_idx..].starts_with("n") {
- // PHP n: Month without leading zeros (1-12)
- let next_sep = fmt_chars.get(f_idx + 1);
- let mut took = 0;
- if let Some(c1) = input_chars.get(i_idx) {
- if c1.is_ascii_digit() {
- took = 1;
- if let Some(c2) = input_chars.get(i_idx + 1) {
- if c2.is_ascii_digit() {
- // Variable length logic
- if let Some(sep) = next_sep {
- if *sep != *c2 {
- took = 2;
- }
- } else {
- took = 2;
- }
- }
- }
+ 'n' => {
+ // PHP n: Month without leading zeros (1-12)
+ let took = find_num_length(input);
+ if took == 0 {
+ return Err("Expected digit for Month".into());
}
+ month = Some(parse_num(&input[..took], "Invalid month".to_string())?);
+ took
}
- if took == 0 {
- return Err("Expected digit for Month".into());
+ 'j' => {
+ // PHP j: Day without leading zeros (1-31)
+ let took = find_num_length(input);
+ if took == 0 {
+ return Err("Expected digit for Day".into());
+ }
+ day = Some(parse_num(&input[..took], "Invalid day".to_string())?);
+ took
}
- let m_str: String = input_chars[i_idx..i_idx + took].iter().collect();
- month = m_str.parse::<u32>().ok();
- f_idx += 1;
- i_idx += took;
- } else if fmt[f_idx..].starts_with("j") {
- // PHP j: Day without leading zeros (1-31)
- let next_sep = fmt_chars.get(f_idx + 1);
- let mut took = 0;
- if let Some(c1) = input_chars.get(i_idx) {
- if c1.is_ascii_digit() {
- took = 1;
- if let Some(c2) = input_chars.get(i_idx + 1) {
- if c2.is_ascii_digit() {
- if let Some(sep) = next_sep {
- if *sep != *c2 {
- took = 2;
- }
- } else {
- took = 2;
- }
- }
+ _ => {
+ // Literal match
+ if let Some(c) = input.chars().next() {
+ if f_char != c {
+ return Err(format!("Expected '{}', found '{}'", f_char, c));
}
+ } else {
+ return Err("Input ends too soon.".to_string());
}
+ 1
}
- if took == 0 {
- return Err("Expected digit for Day".into());
- }
- let d_str: String = input_chars[i_idx..i_idx + took].iter().collect();
- day = d_str.parse::<u32>().ok();
- f_idx += 1;
- i_idx += took;
- } else {
- // Literal match
- if fmt_chars[f_idx] != input_chars[i_idx] {
- return Err(format!(
- "Expected '{}', found '{}'",
- fmt_chars[f_idx], input_chars[i_idx]
- ));
- }
- f_idx += 1;
- i_idx += 1;
- }
+ };
+ input = &input[took..];
}
- if i_idx != input_chars.len() {
+ if !input.is_empty() {
return Err("Input longer than format".into());
}
@@ -401,6 +370,17 @@ mod tests {
let d4 = PlainDate::from_format("01/02/2023", "m/d/Y").unwrap();
assert_eq!(d4.month(), 0);
assert_eq!(d4.day(), 2);
+
+ assert!(PlainDate::from_format("01/02/zzzz", "m/d/Y").is_err());
+ assert!(PlainDate::from_format("01/yy/2023", "m/d/Y").is_err());
+ assert!(PlainDate::from_format("xx/02/zzzz", "m/d/Y").is_err());
+ assert!(PlainDate::from_format("01/yy/zzzz", "m/d/Y").is_err());
+ assert!(PlainDate::from_format("xx/02/zzzz", "m/d/Y").is_err());
+ assert!(PlainDate::from_format("xx/yy/2023", "m/d/Y").is_err());
+ assert!(PlainDate::from_format("xx/yy/zzzz", "m/d/Y").is_err());
+
+ assert!(PlainDate::from_format("xx01/02/2023", "mm/d/Y").is_err());
+ assert!(PlainDate::from_format("1022023", "nmY").is_err())
}
#[test]
--
2.47.3
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
next reply other threads:[~2025-12-17 10:29 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-12-17 10:30 Dominik Csapak [this message]
2025-12-17 10:30 ` [yew-devel] [PATCH yew-widget-toolkit 2/2] plain date: add some useful methods Dominik Csapak
2025-12-17 11:31 ` [yew-devel] applied: [PATCH yew-widget-toolkit 1/2] plain date: improve format parser Dietmar Maurer
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20251217103017.1103164-1-d.csapak@proxmox.com \
--to=d.csapak@proxmox.com \
--cc=yew-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox