1use chrono::{FixedOffset, TimeZone};
2use nu_cmd_base::input_handler::{CmdArgument, operate};
3use nu_engine::command_prelude::*;
4
5use nu_utils::get_system_locale;
6
7struct Arguments {
8 radix: u32,
9 cell_paths: Option<Vec<CellPath>>,
10 signed: bool,
11 little_endian: bool,
12}
13
14impl CmdArgument for Arguments {
15 fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
16 self.cell_paths.take()
17 }
18}
19
20#[derive(Clone)]
21pub struct IntoInt;
22
23impl Command for IntoInt {
24 fn name(&self) -> &str {
25 "into int"
26 }
27
28 fn signature(&self) -> Signature {
29 Signature::build("into int")
30 .input_output_types(vec![
31 (Type::String, Type::Int),
32 (Type::Number, Type::Int),
33 (Type::Bool, Type::Int),
34 (Type::Date, Type::Int),
36 (Type::Duration, Type::Int),
37 (Type::Filesize, Type::Int),
38 (Type::Binary, Type::Int),
39 (Type::table(), Type::table()),
40 (Type::record(), Type::record()),
41 (
42 Type::List(Box::new(Type::String)),
43 Type::List(Box::new(Type::Int)),
44 ),
45 (
46 Type::List(Box::new(Type::Number)),
47 Type::List(Box::new(Type::Int)),
48 ),
49 (
50 Type::List(Box::new(Type::Bool)),
51 Type::List(Box::new(Type::Int)),
52 ),
53 (
54 Type::List(Box::new(Type::Date)),
55 Type::List(Box::new(Type::Int)),
56 ),
57 (
58 Type::List(Box::new(Type::Duration)),
59 Type::List(Box::new(Type::Int)),
60 ),
61 (
62 Type::List(Box::new(Type::Filesize)),
63 Type::List(Box::new(Type::Int)),
64 ),
65 (
67 Type::List(Box::new(Type::Any)),
68 Type::List(Box::new(Type::Int)),
69 ),
70 ])
71 .allow_variants_without_examples(true)
72 .named("radix", SyntaxShape::Number, "radix of integer", Some('r'))
73 .named(
74 "endian",
75 SyntaxShape::String,
76 "byte encode endian, available options: native(default), little, big",
77 Some('e'),
78 )
79 .switch(
80 "signed",
81 "always treat input number as a signed number",
82 Some('s'),
83 )
84 .rest(
85 "rest",
86 SyntaxShape::CellPath,
87 "For a data structure input, convert data at the given cell paths.",
88 )
89 .category(Category::Conversions)
90 }
91
92 fn description(&self) -> &str {
93 "Convert value to integer."
94 }
95
96 fn search_terms(&self) -> Vec<&str> {
97 vec!["convert", "number", "natural"]
98 }
99
100 fn run(
101 &self,
102 engine_state: &EngineState,
103 stack: &mut Stack,
104 call: &Call,
105 input: PipelineData,
106 ) -> Result<PipelineData, ShellError> {
107 let cell_paths = call.rest(engine_state, stack, 0)?;
108 let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
109
110 let radix = call.get_flag::<Value>(engine_state, stack, "radix")?;
111 let radix: u32 = match radix {
112 Some(val) => {
113 let span = val.span();
114 match val {
115 Value::Int { val, .. } => {
116 if !(2..=36).contains(&val) {
117 return Err(ShellError::TypeMismatch {
118 err_message: "Radix must lie in the range [2, 36]".to_string(),
119 span,
120 });
121 }
122 val as u32
123 }
124 _ => 10,
125 }
126 }
127 None => 10,
128 };
129
130 let endian = call.get_flag::<Value>(engine_state, stack, "endian")?;
131 let little_endian = match endian {
132 Some(val) => {
133 let span = val.span();
134 match val {
135 Value::String { val, .. } => match val.as_str() {
136 "native" => cfg!(target_endian = "little"),
137 "little" => true,
138 "big" => false,
139 _ => {
140 return Err(ShellError::TypeMismatch {
141 err_message: "Endian must be one of native, little, big"
142 .to_string(),
143 span,
144 });
145 }
146 },
147 _ => false,
148 }
149 }
150 None => cfg!(target_endian = "little"),
151 };
152
153 let signed = call.has_flag(engine_state, stack, "signed")?;
154
155 let args = Arguments {
156 radix,
157 little_endian,
158 signed,
159 cell_paths,
160 };
161 operate(action, args, input, call.head, engine_state.signals())
162 }
163
164 fn examples(&self) -> Vec<Example> {
165 vec![
166 Example {
167 description: "Convert string to int in table",
168 example: "[[num]; ['-5'] [4] [1.5]] | into int num",
169 result: None,
170 },
171 Example {
172 description: "Convert string to int",
173 example: "'2' | into int",
174 result: Some(Value::test_int(2)),
175 },
176 Example {
177 description: "Convert float to int",
178 example: "5.9 | into int",
179 result: Some(Value::test_int(5)),
180 },
181 Example {
182 description: "Convert decimal string to int",
183 example: "'5.9' | into int",
184 result: Some(Value::test_int(5)),
185 },
186 Example {
187 description: "Convert file size to int",
188 example: "4KB | into int",
189 result: Some(Value::test_int(4000)),
190 },
191 Example {
192 description: "Convert bool to int",
193 example: "[false, true] | into int",
194 result: Some(Value::list(
195 vec![Value::test_int(0), Value::test_int(1)],
196 Span::test_data(),
197 )),
198 },
199 Example {
200 description: "Convert date to int (Unix nanosecond timestamp)",
201 example: "1983-04-13T12:09:14.123456789-05:00 | into int",
202 result: Some(Value::test_int(419101754123456789)),
203 },
204 Example {
205 description: "Convert to int from binary data (radix: 2)",
206 example: "'1101' | into int --radix 2",
207 result: Some(Value::test_int(13)),
208 },
209 Example {
210 description: "Convert to int from hex",
211 example: "'FF' | into int --radix 16",
212 result: Some(Value::test_int(255)),
213 },
214 Example {
215 description: "Convert octal string to int",
216 example: "'0o10132' | into int",
217 result: Some(Value::test_int(4186)),
218 },
219 Example {
220 description: "Convert 0 padded string to int",
221 example: "'0010132' | into int",
222 result: Some(Value::test_int(10132)),
223 },
224 Example {
225 description: "Convert 0 padded string to int with radix 8",
226 example: "'0010132' | into int --radix 8",
227 result: Some(Value::test_int(4186)),
228 },
229 Example {
230 description: "Convert binary value to int",
231 example: "0x[10] | into int",
232 result: Some(Value::test_int(16)),
233 },
234 Example {
235 description: "Convert binary value to signed int",
236 example: "0x[a0] | into int --signed",
237 result: Some(Value::test_int(-96)),
238 },
239 ]
240 }
241}
242
243fn action(input: &Value, args: &Arguments, head: Span) -> Value {
244 let radix = args.radix;
245 let signed = args.signed;
246 let little_endian = args.little_endian;
247 let val_span = input.span();
248
249 match input {
250 Value::Int { val: _, .. } => {
251 if radix == 10 {
252 input.clone()
253 } else {
254 convert_int(input, head, radix)
255 }
256 }
257 Value::Filesize { val, .. } => Value::int(val.get(), head),
258 Value::Float { val, .. } => Value::int(
259 {
260 if radix == 10 {
261 *val as i64
262 } else {
263 match convert_int(&Value::int(*val as i64, head), head, radix).as_int() {
264 Ok(v) => v,
265 _ => {
266 return Value::error(
267 ShellError::CantConvert {
268 to_type: "float".to_string(),
269 from_type: "int".to_string(),
270 span: head,
271 help: None,
272 },
273 head,
274 );
275 }
276 }
277 }
278 },
279 head,
280 ),
281 Value::String { val, .. } => {
282 if radix == 10 {
283 match int_from_string(val, head) {
284 Ok(val) => Value::int(val, head),
285 Err(error) => Value::error(error, head),
286 }
287 } else {
288 convert_int(input, head, radix)
289 }
290 }
291 Value::Bool { val, .. } => {
292 if *val {
293 Value::int(1, head)
294 } else {
295 Value::int(0, head)
296 }
297 }
298 Value::Date { val, .. } => {
299 if val
300 < &FixedOffset::east_opt(0)
301 .expect("constant")
302 .with_ymd_and_hms(1677, 9, 21, 0, 12, 44)
303 .unwrap()
304 || val
305 > &FixedOffset::east_opt(0)
306 .expect("constant")
307 .with_ymd_and_hms(2262, 4, 11, 23, 47, 16)
308 .unwrap()
309 {
310 Value::error (
311 ShellError::IncorrectValue {
312 msg: "DateTime out of range for timestamp: 1677-09-21T00:12:43Z to 2262-04-11T23:47:16".to_string(),
313 val_span,
314 call_span: head,
315 },
316 head,
317 )
318 } else {
319 Value::int(val.timestamp_nanos_opt().unwrap_or_default(), head)
320 }
321 }
322 Value::Duration { val, .. } => Value::int(*val, head),
323 Value::Binary { val, .. } => {
324 use byteorder::{BigEndian, ByteOrder, LittleEndian};
325
326 let mut val = val.to_vec();
327 let size = val.len();
328
329 if size == 0 {
330 return Value::int(0, head);
331 }
332
333 if size > 8 {
334 return Value::error(
335 ShellError::IncorrectValue {
336 msg: format!("binary input is too large to convert to int ({size} bytes)"),
337 val_span,
338 call_span: head,
339 },
340 head,
341 );
342 }
343
344 match (little_endian, signed) {
345 (true, true) => Value::int(LittleEndian::read_int(&val, size), head),
346 (false, true) => Value::int(BigEndian::read_int(&val, size), head),
347 (true, false) => {
348 while val.len() < 8 {
349 val.push(0);
350 }
351 val.resize(8, 0);
352
353 Value::int(LittleEndian::read_i64(&val), head)
354 }
355 (false, false) => {
356 while val.len() < 8 {
357 val.insert(0, 0);
358 }
359 val.resize(8, 0);
360
361 Value::int(BigEndian::read_i64(&val), head)
362 }
363 }
364 }
365 Value::Error { .. } => input.clone(),
367 other => Value::error(
368 ShellError::OnlySupportsThisInputType {
369 exp_input_type: "int, float, filesize, date, string, binary, duration, or bool"
370 .into(),
371 wrong_type: other.get_type().to_string(),
372 dst_span: head,
373 src_span: other.span(),
374 },
375 head,
376 ),
377 }
378}
379
380fn convert_int(input: &Value, head: Span, radix: u32) -> Value {
381 let i = match input {
382 Value::Int { val, .. } => val.to_string(),
383 Value::String { val, .. } => {
384 let val = val.trim();
385 if val.starts_with("0x") || val.starts_with("0b") || val.starts_with("0o")
388 {
390 match int_from_string(val, head) {
391 Ok(x) => return Value::int(x, head),
392 Err(e) => return Value::error(e, head),
393 }
394 } else if val.starts_with("00") {
395 match i64::from_str_radix(val, radix) {
397 Ok(n) => return Value::int(n, head),
398 Err(e) => {
399 return Value::error(
400 ShellError::CantConvert {
401 to_type: "string".to_string(),
402 from_type: "int".to_string(),
403 span: head,
404 help: Some(e.to_string()),
405 },
406 head,
407 );
408 }
409 }
410 }
411 val.to_string()
412 }
413 Value::Error { .. } => return input.clone(),
415 other => {
416 return Value::error(
417 ShellError::OnlySupportsThisInputType {
418 exp_input_type: "string and int".into(),
419 wrong_type: other.get_type().to_string(),
420 dst_span: head,
421 src_span: other.span(),
422 },
423 head,
424 );
425 }
426 };
427 match i64::from_str_radix(i.trim(), radix) {
428 Ok(n) => Value::int(n, head),
429 Err(_reason) => Value::error(
430 ShellError::CantConvert {
431 to_type: "string".to_string(),
432 from_type: "int".to_string(),
433 span: head,
434 help: None,
435 },
436 head,
437 ),
438 }
439}
440
441fn int_from_string(a_string: &str, span: Span) -> Result<i64, ShellError> {
442 let locale = get_system_locale();
444
445 let no_comma_string = a_string.replace(locale.separator(), "");
448
449 let trimmed = no_comma_string.trim();
450 match trimmed {
451 b if b.starts_with("0b") => {
452 let num = match i64::from_str_radix(b.trim_start_matches("0b"), 2) {
453 Ok(n) => n,
454 Err(_reason) => {
455 return Err(ShellError::CantConvert {
456 to_type: "int".to_string(),
457 from_type: "string".to_string(),
458 span,
459 help: Some(r#"digits following "0b" can only be 0 or 1"#.to_string()),
460 });
461 }
462 };
463 Ok(num)
464 }
465 h if h.starts_with("0x") => {
466 let num =
467 match i64::from_str_radix(h.trim_start_matches("0x"), 16) {
468 Ok(n) => n,
469 Err(_reason) => return Err(ShellError::CantConvert {
470 to_type: "int".to_string(),
471 from_type: "string".to_string(),
472 span,
473 help: Some(
474 r#"hexadecimal digits following "0x" should be in 0-9, a-f, or A-F"#
475 .to_string(),
476 ),
477 }),
478 };
479 Ok(num)
480 }
481 o if o.starts_with("0o") => {
482 let num = match i64::from_str_radix(o.trim_start_matches("0o"), 8) {
483 Ok(n) => n,
484 Err(_reason) => {
485 return Err(ShellError::CantConvert {
486 to_type: "int".to_string(),
487 from_type: "string".to_string(),
488 span,
489 help: Some(r#"octal digits following "0o" should be in 0-7"#.to_string()),
490 });
491 }
492 };
493 Ok(num)
494 }
495 _ => match trimmed.parse::<i64>() {
496 Ok(n) => Ok(n),
497 Err(_) => match a_string.parse::<f64>() {
498 Ok(f) => Ok(f as i64),
499 _ => Err(ShellError::CantConvert {
500 to_type: "int".to_string(),
501 from_type: "string".to_string(),
502 span,
503 help: Some(format!(
504 r#"string "{trimmed}" does not represent a valid integer"#
505 )),
506 }),
507 },
508 },
509 }
510}
511
512#[cfg(test)]
513mod test {
514 use chrono::{DateTime, FixedOffset};
515 use rstest::rstest;
516
517 use super::Value;
518 use super::*;
519 use nu_protocol::Type::Error;
520
521 #[test]
522 fn test_examples() {
523 use crate::test_examples;
524
525 test_examples(IntoInt {})
526 }
527
528 #[test]
529 fn turns_to_integer() {
530 let word = Value::test_string("10");
531 let expected = Value::test_int(10);
532
533 let actual = action(
534 &word,
535 &Arguments {
536 radix: 10,
537 cell_paths: None,
538 signed: false,
539 little_endian: false,
540 },
541 Span::test_data(),
542 );
543 assert_eq!(actual, expected);
544 }
545
546 #[test]
547 fn turns_binary_to_integer() {
548 let s = Value::test_string("0b101");
549 let actual = action(
550 &s,
551 &Arguments {
552 radix: 10,
553 cell_paths: None,
554 signed: false,
555 little_endian: false,
556 },
557 Span::test_data(),
558 );
559 assert_eq!(actual, Value::test_int(5));
560 }
561
562 #[test]
563 fn turns_hex_to_integer() {
564 let s = Value::test_string("0xFF");
565 let actual = action(
566 &s,
567 &Arguments {
568 radix: 16,
569 cell_paths: None,
570 signed: false,
571 little_endian: false,
572 },
573 Span::test_data(),
574 );
575 assert_eq!(actual, Value::test_int(255));
576 }
577
578 #[test]
579 fn communicates_parsing_error_given_an_invalid_integerlike_string() {
580 let integer_str = Value::test_string("36anra");
581
582 let actual = action(
583 &integer_str,
584 &Arguments {
585 radix: 10,
586 cell_paths: None,
587 signed: false,
588 little_endian: false,
589 },
590 Span::test_data(),
591 );
592
593 assert_eq!(actual.get_type(), Error)
594 }
595
596 #[rstest]
597 #[case("2262-04-11T23:47:16+00:00", 0x7fff_ffff_ffff_ffff)]
598 #[case("1970-01-01T00:00:00+00:00", 0)]
599 #[case("1677-09-21T00:12:44+00:00", -0x7fff_ffff_ffff_ffff)]
600 fn datetime_to_int_values_that_work(
601 #[case] dt_in: DateTime<FixedOffset>,
602 #[case] int_expected: i64,
603 ) {
604 let s = Value::test_date(dt_in);
605 let actual = action(
606 &s,
607 &Arguments {
608 radix: 10,
609 cell_paths: None,
610 signed: false,
611 little_endian: false,
612 },
613 Span::test_data(),
614 );
615 let exp_truncated = (int_expected / 1_000_000_000) * 1_000_000_000;
617 assert_eq!(actual, Value::test_int(exp_truncated));
618 }
619
620 #[rstest]
621 #[case("2262-04-11T23:47:17+00:00", "DateTime out of range for timestamp")]
622 #[case("1677-09-21T00:12:43+00:00", "DateTime out of range for timestamp")]
623 fn datetime_to_int_values_that_fail(
624 #[case] dt_in: DateTime<FixedOffset>,
625 #[case] err_expected: &str,
626 ) {
627 let s = Value::test_date(dt_in);
628 let actual = action(
629 &s,
630 &Arguments {
631 radix: 10,
632 cell_paths: None,
633 signed: false,
634 little_endian: false,
635 },
636 Span::test_data(),
637 );
638 if let Value::Error { error, .. } = actual {
639 if let ShellError::IncorrectValue { msg: e, .. } = *error {
640 assert!(
641 e.contains(err_expected),
642 "{e:?} doesn't contain {err_expected}"
643 );
644 } else {
645 panic!("Unexpected error variant {error:?}")
646 }
647 } else {
648 panic!("Unexpected actual value {actual:?}")
649 }
650 }
651}