public inbox for yew-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [yew-devel] [PATCH yew-widget-toolkit 1/2] plain date: improve format parser
@ 2025-12-17 10:30 Dominik Csapak
  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
  0 siblings, 2 replies; 3+ messages in thread
From: Dominik Csapak @ 2025-12-17 10:30 UTC (permalink / raw)
  To: yew-devel

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


^ permalink raw reply	[flat|nested] 3+ messages in thread

* [yew-devel] [PATCH yew-widget-toolkit 2/2] plain date: add some useful methods
  2025-12-17 10:30 [yew-devel] [PATCH yew-widget-toolkit 1/2] plain date: improve format parser Dominik Csapak
@ 2025-12-17 10:30 ` Dominik Csapak
  2025-12-17 11:31 ` [yew-devel] applied: [PATCH yew-widget-toolkit 1/2] plain date: improve format parser Dietmar Maurer
  1 sibling, 0 replies; 3+ messages in thread
From: Dominik Csapak @ 2025-12-17 10:30 UTC (permalink / raw)
  To: yew-devel

these reuse the also new 'to_date' which can be handy if one needs to
work with javascript date objects.

In the future we should replace that with the equivalent temporal api
types.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 src/widget/form/date_field/plain_date.rs | 29 ++++++++++++++++++------
 1 file changed, 22 insertions(+), 7 deletions(-)

diff --git a/src/widget/form/date_field/plain_date.rs b/src/widget/form/date_field/plain_date.rs
index e7d8a97..107c5f9 100644
--- a/src/widget/form/date_field/plain_date.rs
+++ b/src/widget/form/date_field/plain_date.rs
@@ -27,17 +27,32 @@ impl PlainDate {
         }
     }
 
+    /// Return a new [`js_sys::Date`] with a time value of 00:00:00.
+    pub fn to_date(&self) -> Date {
+        Date::new_with_year_month_day(self.year as u32, self.month as i32, self.day as i32)
+    }
+
     /// Convert to a timestamp (milliseconds).
     /// Returns the timestamp for 12:00:00 (Noon) local time on this date.
     pub fn to_timestamp(&self) -> f64 {
-        let d = Date::new_0();
-        d.set_full_year(self.year as u32);
-        d.set_month(self.month);
-        d.set_date(self.day);
+        let d = self.to_date();
         d.set_hours(12);
-        d.set_minutes(0);
-        d.set_seconds(0);
-        d.set_milliseconds(0);
+        d.get_time()
+    }
+
+    /// Convert to a timestamp (milliseconds).
+    /// Returns the timestamp for 00:00:00 local time on this date.
+    pub fn to_timestamp_start(&self) -> f64 {
+        self.to_date().get_time()
+    }
+
+    /// Convert to a timestamp (milliseconds).
+    /// Returns the timestamp for 23:59:59 local time on this date.
+    pub fn to_timestamp_end(&self) -> f64 {
+        let d = self.to_date();
+        d.set_hours(23);
+        d.set_minutes(59);
+        d.set_seconds(59);
         d.get_time()
     }
 
-- 
2.47.3



_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel


^ permalink raw reply	[flat|nested] 3+ messages in thread

* [yew-devel] applied: [PATCH yew-widget-toolkit 1/2] plain date: improve format parser
  2025-12-17 10:30 [yew-devel] [PATCH yew-widget-toolkit 1/2] plain date: improve format parser Dominik Csapak
  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 ` Dietmar Maurer
  1 sibling, 0 replies; 3+ messages in thread
From: Dietmar Maurer @ 2025-12-17 11:31 UTC (permalink / raw)
  To: Yew framework devel list at Proxmox, Dominik Csapak

applied both patches




_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel


^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2025-12-17 11:30 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-12-17 10:30 [yew-devel] [PATCH yew-widget-toolkit 1/2] plain date: improve format parser Dominik Csapak
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

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal