1use std::str::Split;
7
8use chrono::DateTime;
9use chrono::NaiveDate;
10use chrono::NaiveDateTime;
11use chrono::NaiveTime;
12use color_eyre::eyre::Context;
13use color_eyre::eyre::eyre;
14use nvim_oxi::Dictionary;
15use strum::EnumIter;
16use strum::IntoEnumIterator;
17
18pub fn dict() -> Dictionary {
24 dict! {
25 "convert_selection": fn_from!(convert_selection),
26 }
27}
28
29fn convert_selection(_: ()) {
48 let Some(selection) = ytil_noxi::visual_selection::get(()) else {
49 return;
50 };
51
52 let opts = ConversionOption::iter();
53
54 let callback = {
55 let opts = opts.clone();
56 move |choice_idx| {
57 let Some(opt) = opts.get(choice_idx) else { return };
58 let Ok(transformed_line) = opt
59 .convert(&selection.lines().to_vec().join("\n"))
62 .inspect_err(|err| {
63 ytil_noxi::notify::error(format!(
64 "error setting lines of buffer | start={:#?} end={:#?} error={err:#?}",
65 selection.start(),
66 selection.end()
67 ));
68 })
69 else {
70 return;
71 };
72 ytil_noxi::buffer::replace_text_and_notify_if_error(&selection, vec![transformed_line]);
73 }
74 };
75
76 if let Err(err) = ytil_noxi::vim_ui_select::open(opts, &[("prompt", "Select conversion ")], callback, None) {
77 ytil_noxi::notify::error(format!("error converting selection | error={err:#?}"));
78 }
79}
80
81#[derive(strum::Display, EnumIter)]
83enum ConversionOption {
84 #[strum(to_string = "RGB to HEX")]
86 RgbToHex,
87 #[strum(to_string = "Datetime formatted strings to chrono parse_from_str code")]
89 DateTimeStrToChronoParseFromStr,
90 #[strum(to_string = "Unix timestamp to ISO 8601 date time")]
91 UnixTimestampToIso8601,
92}
93
94impl ConversionOption {
95 pub fn convert(&self, selection: &str) -> color_eyre::Result<String> {
96 match self {
97 Self::RgbToHex => rgb_to_hex(selection),
98 Self::DateTimeStrToChronoParseFromStr => date_time_str_to_chrono_parse_from_str(selection),
99 Self::UnixTimestampToIso8601 => unix_timestamp_to_iso_8601_date_time(selection),
100 }
101 }
102}
103
104fn rgb_to_hex(input: &str) -> color_eyre::Result<String> {
112 fn u8_color_code_from_rgb_split(rgb: &mut Split<'_, char>, color: &str) -> color_eyre::Result<u8> {
113 rgb.next()
114 .ok_or_else(|| eyre!("missing color component {color}"))
115 .and_then(|s| {
116 s.trim()
117 .parse::<u8>()
118 .wrap_err_with(|| format!("cannot parse str as u8 color code | str={s:?}"))
119 })
120 }
121
122 let mut rgb_split = input.split(',');
123 let r = u8_color_code_from_rgb_split(&mut rgb_split, "R")?;
124 let g = u8_color_code_from_rgb_split(&mut rgb_split, "G")?;
125 let b = u8_color_code_from_rgb_split(&mut rgb_split, "B")?;
126
127 Ok(format!("#{r:02x}{g:02x}{b:02x}"))
128}
129
130fn date_time_str_to_chrono_parse_from_str(input: &str) -> color_eyre::Result<String> {
141 if DateTime::parse_from_str(input, "%d-%m-%Y,%H:%M:%S%z").is_ok() {
142 return Ok(format!(
143 r#"DateTime::parse_from_str("{input}", "%d-%m-%Y,%H:%M:%S%Z").unwrap()"#
144 ));
145 }
146 if NaiveDateTime::parse_from_str(input, "%d-%m-%Y,%H:%M:%S").is_ok() {
147 return Ok(format!(
148 r#"NaiveDateTime::parse_from_str("{input}", "%d-%m-%Y,%H:%M:%S").unwrap()"#
149 ));
150 }
151 if NaiveDate::parse_from_str(input, "%d-%m-%Y").is_ok() {
152 return Ok(format!(r#"NaiveDate::parse_from_str("{input}", "%d-%m-%Y").unwrap()"#));
153 }
154 if NaiveTime::parse_from_str(input, "%H:%M:%S").is_ok() {
155 return Ok(format!(r#"NaiveTime::parse_from_str("{input}", "%H:%M:%S").unwrap()"#));
156 }
157 Err(eyre!(
158 "cannot get chrono parse_from_str for supplied input | input={input:?}"
159 ))
160}
161
162fn unix_timestamp_to_iso_8601_date_time(input: &str) -> color_eyre::Result<String> {
163 input
164 .parse::<i64>()
165 .wrap_err_with(|| format!("cannot convert input to i64 | input={input:?}"))
166 .and_then(|timestamp| {
167 DateTime::from_timestamp_secs(timestamp)
168 .ok_or_else(|| eyre!("cannot convert timestamp to DateTime<Utc> | timestamp={timestamp}"))
169 })
170 .map(|dt| dt.to_rfc3339())
171}
172
173#[cfg(test)]
174mod tests {
175 use rstest::rstest;
176
177 use super::*;
178
179 #[rstest]
180 #[case::red("255,0,0", "#ff0000")]
181 #[case::red_with_spaces(" 255 , 0 , 0 ", "#ff0000")]
182 #[case::black("0,0,0", "#000000")]
183 #[case::white("255,255,255", "#ffffff")]
184 #[case::red_with_extra_component("255,0,0,123", "#ff0000")]
185 fn rgb_to_hex_when_valid_rgb_returns_hex(#[case] input: &str, #[case] expected: &str) {
186 assert2::let_assert!(Ok(actual) = rgb_to_hex(input));
187 pretty_assertions::assert_eq!(actual, expected);
188 }
189
190 #[rstest]
191 #[case::empty_input("", "cannot parse str as u8 color code | str=\"\"")]
192 #[case::single_component("0", "missing color component G")]
193 #[case::two_components("255,0", "missing color component B")]
194 #[case::out_of_range_red("256,0,0", "cannot parse str as u8 color code | str=\"256\"")]
195 #[case::invalid_green("255,abc,0", "cannot parse str as u8 color code | str=\"abc\"")]
196 #[case::invalid_blue("255,0,def", "cannot parse str as u8 color code | str=\"def\"")]
197 fn rgb_to_hex_when_invalid_input_returns_error(#[case] input: &str, #[case] expected_error: &str) {
198 let result = rgb_to_hex(input);
199 assert2::let_assert!(Err(err) = result);
200 pretty_assertions::assert_eq!(err.to_string(), expected_error);
201 }
202
203 #[rstest]
204 #[case::datetime_with_offset(
205 "25-12-2023,14:30:45+00:00",
206 r#"DateTime::parse_from_str("25-12-2023,14:30:45+00:00", "%d-%m-%Y,%H:%M:%S%Z").unwrap()"#
207 )]
208 #[case::naive_datetime(
209 "25-12-2023,14:30:45",
210 r#"NaiveDateTime::parse_from_str("25-12-2023,14:30:45", "%d-%m-%Y,%H:%M:%S").unwrap()"#
211 )]
212 #[case::naive_date("25-12-2023", r#"NaiveDate::parse_from_str("25-12-2023", "%d-%m-%Y").unwrap()"#)]
213 #[case::naive_time("14:30:45", r#"NaiveTime::parse_from_str("14:30:45", "%H:%M:%S").unwrap()"#)]
214 fn date_time_str_to_chrono_parse_from_str_when_valid_input_returns_correct_code(
215 #[case] input: &str,
216 #[case] expected: &str,
217 ) {
218 assert2::let_assert!(Ok(actual) = date_time_str_to_chrono_parse_from_str(input));
219 pretty_assertions::assert_eq!(actual, expected);
220 }
221
222 #[test]
223 fn date_time_str_to_chrono_parse_from_str_when_invalid_input_returns_error() {
224 assert2::let_assert!(Err(err) = date_time_str_to_chrono_parse_from_str("invalid"));
225 pretty_assertions::assert_eq!(
226 err.to_string(),
227 "cannot get chrono parse_from_str for supplied input | input=\"invalid\""
228 );
229 }
230
231 #[rstest]
232 #[case::non_numeric_input("abc", "cannot convert input to i64 | input=\"abc\"")]
233 #[case::empty_input("", "cannot convert input to i64 | input=\"\"")]
234 fn unix_timestamp_to_iso_8601_date_time_when_invalid_input_returns_error(
235 #[case] input: &str,
236 #[case] expected_error: &str,
237 ) {
238 let result = unix_timestamp_to_iso_8601_date_time(input);
239 assert2::let_assert!(Err(err) = result);
240 pretty_assertions::assert_eq!(err.to_string(), expected_error);
241 }
242}