1use std::str::Split;
7
8use chrono::DateTime;
9use chrono::NaiveDate;
10use chrono::NaiveDateTime;
11use chrono::NaiveTime;
12use nvim_oxi::Dictionary;
13use rootcause::prelude::ResultExt;
14use rootcause::report;
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) -> rootcause::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) -> rootcause::Result<String> {
112 fn u8_color_code_from_rgb_split(rgb: &mut Split<'_, char>, color: &str) -> rootcause::Result<u8> {
113 let s = rgb.next().ok_or_else(|| report!("missing color component {color}"))?;
114 Ok(s.trim()
115 .parse::<u8>()
116 .context("cannot parse str as u8 color code")
117 .attach_with(|| format!("str={s:?}"))?)
118 }
119
120 let mut rgb_split = input.split(',');
121 let r = u8_color_code_from_rgb_split(&mut rgb_split, "R")?;
122 let g = u8_color_code_from_rgb_split(&mut rgb_split, "G")?;
123 let b = u8_color_code_from_rgb_split(&mut rgb_split, "B")?;
124
125 Ok(format!("#{r:02x}{g:02x}{b:02x}"))
126}
127
128fn date_time_str_to_chrono_parse_from_str(input: &str) -> rootcause::Result<String> {
139 if DateTime::parse_from_str(input, "%d-%m-%Y,%H:%M:%S%z").is_ok() {
140 return Ok(format!(
141 r#"DateTime::parse_from_str("{input}", "%d-%m-%Y,%H:%M:%S%Z").unwrap()"#
142 ));
143 }
144 if NaiveDateTime::parse_from_str(input, "%d-%m-%Y,%H:%M:%S").is_ok() {
145 return Ok(format!(
146 r#"NaiveDateTime::parse_from_str("{input}", "%d-%m-%Y,%H:%M:%S").unwrap()"#
147 ));
148 }
149 if NaiveDate::parse_from_str(input, "%d-%m-%Y").is_ok() {
150 return Ok(format!(r#"NaiveDate::parse_from_str("{input}", "%d-%m-%Y").unwrap()"#));
151 }
152 if NaiveTime::parse_from_str(input, "%H:%M:%S").is_ok() {
153 return Ok(format!(r#"NaiveTime::parse_from_str("{input}", "%H:%M:%S").unwrap()"#));
154 }
155 Err(report!("cannot get chrono parse_from_str for supplied input").attach(format!("input={input:?}")))
156}
157
158fn unix_timestamp_to_iso_8601_date_time(input: &str) -> rootcause::Result<String> {
159 let timestamp = input
160 .parse::<i64>()
161 .context("cannot convert input to i64")
162 .attach_with(|| format!("input={input:?}"))?;
163 let dt = DateTime::from_timestamp_secs(timestamp)
164 .ok_or_else(|| report!("cannot convert timestamp to DateTime<Utc>"))
165 .attach_with(|| format!("timestamp={timestamp}"))?;
166 Ok(dt.to_rfc3339())
167}
168
169#[cfg(test)]
170mod tests {
171 use rstest::rstest;
172
173 use super::*;
174
175 #[rstest]
176 #[case::red("255,0,0", "#ff0000")]
177 #[case::red_with_spaces(" 255 , 0 , 0 ", "#ff0000")]
178 #[case::black("0,0,0", "#000000")]
179 #[case::white("255,255,255", "#ffffff")]
180 #[case::red_with_extra_component("255,0,0,123", "#ff0000")]
181 fn rgb_to_hex_when_valid_rgb_returns_hex(#[case] input: &str, #[case] expected: &str) {
182 assert2::assert!(let Ok(actual) = rgb_to_hex(input));
183 pretty_assertions::assert_eq!(actual, expected);
184 }
185
186 #[rstest]
187 #[case::empty_input("", "cannot parse str as u8 color code")]
188 #[case::single_component("0", "missing color component G")]
189 #[case::two_components("255,0", "missing color component B")]
190 #[case::out_of_range_red("256,0,0", "cannot parse str as u8 color code")]
191 #[case::invalid_green("255,abc,0", "cannot parse str as u8 color code")]
192 #[case::invalid_blue("255,0,def", "cannot parse str as u8 color code")]
193 fn rgb_to_hex_when_invalid_input_returns_error(#[case] input: &str, #[case] expected_ctx: &str) {
194 assert2::assert!(let Err(err) = rgb_to_hex(input));
195 assert_eq!(err.format_current_context().to_string(), expected_ctx);
196 }
197
198 #[rstest]
199 #[case::datetime_with_offset(
200 "25-12-2023,14:30:45+00:00",
201 r#"DateTime::parse_from_str("25-12-2023,14:30:45+00:00", "%d-%m-%Y,%H:%M:%S%Z").unwrap()"#
202 )]
203 #[case::naive_datetime(
204 "25-12-2023,14:30:45",
205 r#"NaiveDateTime::parse_from_str("25-12-2023,14:30:45", "%d-%m-%Y,%H:%M:%S").unwrap()"#
206 )]
207 #[case::naive_date("25-12-2023", r#"NaiveDate::parse_from_str("25-12-2023", "%d-%m-%Y").unwrap()"#)]
208 #[case::naive_time("14:30:45", r#"NaiveTime::parse_from_str("14:30:45", "%H:%M:%S").unwrap()"#)]
209 fn date_time_str_to_chrono_parse_from_str_when_valid_input_returns_correct_code(
210 #[case] input: &str,
211 #[case] expected: &str,
212 ) {
213 assert2::assert!(let Ok(actual) = date_time_str_to_chrono_parse_from_str(input));
214 pretty_assertions::assert_eq!(actual, expected);
215 }
216
217 #[test]
218 fn date_time_str_to_chrono_parse_from_str_when_invalid_input_returns_error() {
219 assert2::assert!(let Err(err) = date_time_str_to_chrono_parse_from_str("invalid"));
220 assert_eq!(
221 err.format_current_context().to_string(),
222 "cannot get chrono parse_from_str for supplied input"
223 );
224 }
225
226 #[rstest]
227 #[case::non_numeric_input("abc", "cannot convert input to i64")]
228 #[case::empty_input("", "cannot convert input to i64")]
229 fn unix_timestamp_to_iso_8601_date_time_when_invalid_input_returns_error(
230 #[case] input: &str,
231 #[case] expected_ctx: &str,
232 ) {
233 assert2::assert!(let Err(err) = unix_timestamp_to_iso_8601_date_time(input));
234 assert_eq!(err.format_current_context().to_string(), expected_ctx);
235 }
236}