Repository: CSML-by-Clevy/csml-engine (rust)

Issue:
add Time().parse()
**Describe the solution you'd like**
It would be very practical if we add the method Time().parse("1996-12-19T16:39:57-08:00") in order to instantiate time easily

the format accepted will be RFC 3339 and ISO 8601 date




Pull request description:
add Time().parse()
Fixes #279 

## Proposed Changes

- add method .pase() to Time()
- parse(String,  Optional<String> ) accepts 2 arguments 
   - if only 1 arg parse() will use RFC 3339 and ISO 8601 for parsing 
   - if 2 args  parse() will use the second argument as a specified format for parsing
      formats: https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html


Patch:
diff --git a/csml_interpreter/src/data/primitive/object.rs b/csml_interpreter/src/data/primitive/object.rs
index 5b3874f4..7ad6e1a4 100644
--- a/csml_interpreter/src/data/primitive/object.rs
+++ b/csml_interpreter/src/data/primitive/object.rs
@@ -74,12 +74,12 @@ lazy_static! {
 
         map.insert(
             "at",
-            (PrimitiveObject::set_date_at as PrimitiveMethod, Right::Read),
+            (PrimitiveObject::set_date_at as PrimitiveMethod, Right::Write),
         );
 
         map.insert(
             "unix",
-            (PrimitiveObject::unix as PrimitiveMethod, Right::Read),
+            (PrimitiveObject::unix as PrimitiveMethod, Right::Write),
         );
 
         map.insert(
@@ -87,6 +87,11 @@ lazy_static! {
             (PrimitiveObject::date_format as PrimitiveMethod, Right::Read),
         );
 
+        map.insert(
+            "parse",
+            (PrimitiveObject::parse_date as PrimitiveMethod, Right::Read),
+        );
+
         map
     };
 }
@@ -603,7 +608,10 @@ impl PrimitiveObject {
                         interval
                     )
                 );
-                Ok(PrimitiveObject::get_literal(&object.value, interval))
+                let mut lit = PrimitiveObject::get_literal(&object.value, interval);
+                lit.set_content_type("time");
+
+                Ok(lit)
             }
             None => Ok(PrimitiveBoolean::get_literal(false, interval))
         }
@@ -640,6 +648,25 @@ impl PrimitiveObject {
         }
     }
 
+    fn parse_date(
+        _object: &mut PrimitiveObject,
+        args: &HashMap<String, Literal>,
+        data: &mut Data,
+        interval: Interval,
+        _content_type: &str,
+    ) -> Result<Literal, ErrorInfo> {
+        match args.len() {
+            1 => tools_time::parse_rfc3339(args, data, interval),
+            len if len >= 2 => tools_time::pasre_from_str(args, data, interval),
+            _ => return Err(gen_error_info(
+                Position::new(interval, &data.context.flow,),
+                format!("usage: expect one ore two arguments :
+                Time().parse(\"2020-08-13\")   or
+                Time().parse(\"1983-08-13 12:09:14.274\", \"%Y-%m-%d %H:%M:%S%.3f\")"),
+            ))
+        }
+    }
+
     fn date_format(
         object: &mut PrimitiveObject,
         args: &HashMap<String, Literal>,
diff --git a/csml_interpreter/src/data/primitive/tools_time.rs b/csml_interpreter/src/data/primitive/tools_time.rs
index e8928f5f..4815fc58 100644
--- a/csml_interpreter/src/data/primitive/tools_time.rs
+++ b/csml_interpreter/src/data/primitive/tools_time.rs
@@ -1,8 +1,44 @@
+use crate::error_format::*;
 use crate::data::{
     Literal, primitive::PrimitiveType,
+    ast::Interval,
+    primitive::{
+        Data, PrimitiveInt, PrimitiveObject
+    },
+    error_info::ErrorInfo,
+    position::Position
 };
 use std::{collections::HashMap};
+use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
 
+////////////////////////////////////////////////////////////////////////////////
+// PRIVATE FUNCTIONS
+////////////////////////////////////////////////////////////////////////////////
+
+fn get_date_string(
+    args: &HashMap<String, Literal>,
+    index: usize,
+    data: &mut Data,
+    interval: Interval,
+    error: &str,
+) -> Result<String, ErrorInfo> {
+    match args.get(&format!("arg{}", index)) {
+        Some(literal) if literal.primitive.get_type() == PrimitiveType::PrimitiveString => {
+            let value = Literal::get_value::<String>(
+                &literal.primitive,
+                &data.context.flow,
+                literal.interval,
+                format!("{}", error),
+            )?;
+
+            Ok(value.to_owned())
+        }
+        _ => return Err(gen_error_info(
+            Position::new(interval, &data.context.flow,),
+            format!("{}", error),
+        ))
+    }
+}
 
 ////////////////////////////////////////////////////////////////////////////////
 // PUBLIC FUNCTIONS
@@ -34,3 +70,107 @@ pub fn get_date(args: &HashMap<String, Literal>) -> [i64; 7] {
 
     date
 }
+
+pub fn parse_rfc3339(
+    args: &HashMap<String, Literal>,
+    data: &mut Data,
+    interval: Interval,
+) -> Result<Literal, ErrorInfo> {
+    let usage = "invalid value, 'parse(String)' expect a valid RFC 3339 and ISO 8601 date and time string such as '1996-12-19T00:00:00Z'";
+
+    let date_str = get_date_string(
+        args,
+        0,
+        data,
+        interval,
+        usage
+    )?;
+
+    // autocomplete format with default values
+    let date_str = match date_str.len() {
+        4 => format!("{}-{a1}-{a1}T{a2}:{a2}:{a2}Z", date_str, a1 = "01", a2 = "00"),
+        7 => format!("{}-{a1}T{a2}:{a2}:{a2}Z", date_str, a1 = "01", a2 = "00"),
+        10 => format!("{}T{a2}:{a2}:{a2}Z", date_str, a2 = "00"),
+        13 => format!("{}:{a2}:{a2}Z", date_str, a2 = "00"),
+        16 => format!("{}:{a2}Z", date_str, a2 = "00"),
+        19 => format!("{}Z", date_str),
+        _ => date_str.to_owned()
+    };
+
+    let date = match DateTime::parse_from_rfc3339(&date_str) {
+        Ok(date) => date,
+        Err(_) => return Err(gen_error_info(
+            Position::new(interval, &data.context.flow,),
+            format!("{}", usage),
+        ))
+    };
+
+    let mut object = HashMap::new();
+
+    object.insert(
+        "milliseconds".to_owned(),
+        PrimitiveInt::get_literal(
+            date.timestamp_millis(),
+            interval
+        )
+    );
+    let mut lit = PrimitiveObject::get_literal(&object, interval);
+    lit.set_content_type("time");
+
+    Ok(lit)
+}
+
+pub fn pasre_from_str(
+    args: &HashMap<String, Literal>,
+    data: &mut Data,
+    interval: Interval,
+) -> Result<Literal, ErrorInfo> {
+    let usage = "invalid value,
+    expect date and a specified format to parse the date example:
+    parse(\"1983 08 13 12:09:14.274\", \"%Y %m %d %H:%M:%S%.3f\")";
+
+    let date_str = get_date_string(
+        args,
+        0,
+        data,
+        interval,
+        usage
+    )?;
+
+    let format_str = get_date_string(
+        args,
+        1,
+        data,
+        interval,
+        usage
+    )?;
+
+    let date_millis = if let Ok(date) = DateTime::parse_from_str(&date_str, &format_str) {
+        date.timestamp_millis()
+    } else if let Ok(naive_datetime) = NaiveDateTime::parse_from_str(&date_str, &format_str) {
+        let date = DateTime::<Utc>::from_utc(naive_datetime, Utc);
+        date.timestamp_millis()
+    } else if let Ok(naive_date) = NaiveDate::parse_from_str(&date_str, &format_str) {
+        let naive_datetime: NaiveDateTime = naive_date.and_hms(0,0,0);
+        let date = DateTime::<Utc>::from_utc(naive_datetime, Utc);
+        date.timestamp_millis()
+    } else {
+        return Err(gen_error_info(
+            Position::new(interval, &data.context.flow,),
+            format!("{}", usage),
+        ))
+    };
+
+    let mut object = HashMap::new();
+    object.insert(
+        "milliseconds".to_owned(),
+        PrimitiveInt::get_literal(
+            date_millis,
+            interval
+        )
+    );
+    let mut lit = PrimitiveObject::get_literal(&object, interval);
+    lit.set_content_type("time");
+
+    Ok(lit)
+}
\ No newline at end of file


Test patch:
diff --git a/csml_interpreter/CSML/basic_test/built-in/time.csml b/csml_interpreter/CSML/basic_test/built-in/time.csml
index f3ee948f..efbd7481 100644
--- a/csml_interpreter/CSML/basic_test/built-in/time.csml
+++ b/csml_interpreter/CSML/basic_test/built-in/time.csml
@@ -8,4 +8,16 @@ start:
     say time.format()
 
     say time.format("%Y")
+    goto end
+
+parse_1_arg:
+    do time = Time().parse("1983-08-13")
+
+    say time.format()
+    goto end
+
+parse_2_arg:
+    do time = Time().parse("1983 08 13 12:09:14.274", "%Y %m %d %H:%M:%S%.3f")
+
+    say time.format()
     goto end
\ No newline at end of file
diff --git a/csml_interpreter/tests/time.rs b/csml_interpreter/tests/time.rs
index 7836f186..f3a80eab 100644
--- a/csml_interpreter/tests/time.rs
+++ b/csml_interpreter/tests/time.rs
@@ -12,13 +12,15 @@ use serde_json::Value;
 #[test]
 fn ok_time() {
     let data =
-        r#"{"messages":[ 
+        r#"
+        {"messages":[ 
             {"content":{"text": "true"},"content_type":"text"},
             {"content":{"text": "true"},"content_type":"text"},
             {"content":{"text": "2014-10-20T01:00:00.000Z"},"content_type":"text"},
             {"content":{"text": "2014"},"content_type":"text"}
         ],
-        "memories":[]}"#;
+        "memories":[]
+        }"#;
     let msg = format_message(
         Event::new("payload", "", serde_json::json!({})),
         Context::new(HashMap::new(), HashMap::new(), None, None, "start", "flow"),
@@ -30,3 +32,48 @@ fn ok_time() {
 
     assert_eq!(v1, v2)
 }
+
+
+#[test]
+fn ok_time_parse_1_args() {
+    let data =
+        r#"
+        {"messages":[ 
+            {"content":{"text": "1983-08-13T00:00:00.000Z"},"content_type":"text"}
+        ],
+        "memories":[]
+        }"#;
+    let msg = format_message(
+        Event::new("payload", "", serde_json::json!({})),
+        Context::new(HashMap::new(), HashMap::new(), None, None, "parse_1_arg", "flow"),
+        "CSML/basic_test/built-in/time.csml",
+    );
+
+    let v1: Value = message_to_json_value(msg);
+    let v2: Value = serde_json::from_str(data).unwrap();
+
+    assert_eq!(v1, v2)
+}
+
+
+#[test]
+fn ok_time_parse_2_args() {
+    let data =
+        r#"
+        {"messages":[ 
+            {"content":{"text": "1983-08-13T12:09:14.274Z"},"content_type":"text"}
+        ],
+        "memories":[]
+        }"#;
+    let msg = format_message(
+        Event::new("payload", "", serde_json::json!({})),
+        Context::new(HashMap::new(), HashMap::new(), None, None, "parse_2_arg", "flow"),
+        "CSML/basic_test/built-in/time.csml",
+    );
+
+    let v1: Value = message_to_json_value(msg);
+    let v2: Value = serde_json::from_str(data).unwrap();
+
+    assert_eq!(v1, v2)
+}
+