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