Skip to main content

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