nu_command/conversions/into/
int.rs

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                // Unix timestamp in nanoseconds
35                (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                // Relaxed case to support heterogeneous lists
66                (
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        // Propagate errors by explicitly matching them before the final case.
366        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") // hex
386                || val.starts_with("0b") // binary
387                || val.starts_with("0o")
388            // octal
389            {
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                // It's a padded string
396                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        // Propagate errors by explicitly matching them before the final case.
414        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    // Get the Locale so we know what the thousands separator is
443    let locale = get_system_locale();
444
445    // Now that we know the locale, get the thousands separator and remove it
446    // so strings like 1,123,456 can be parsed as 1123456
447    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        // ignore fractional seconds -- I don't want to hard code test values that might vary due to leap nanoseconds.
616        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}