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