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 (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 (
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 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") || val.starts_with("0b") || val.starts_with("0o")
365 {
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 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 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 let locale = get_system_locale();
421
422 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 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}