1use chrono::{FixedOffset, TimeZone};
2use nu_cmd_base::input_handler::{CmdArgument, operate};
3use nu_engine::command_prelude::*;
4
5use nu_heavy_utils::endian::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 { .. } => {
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 size = val.len();
304
305 if size == 0 {
306 return Value::int(0, head);
307 }
308
309 if size > 8 {
310 return Value::error(
311 ShellError::IncorrectValue {
312 msg: format!("binary input is too large to convert to int ({size} bytes)"),
313 val_span,
314 call_span: head,
315 },
316 head,
317 );
318 }
319
320 let val = match (endian, signed) {
321 (Endian::Little, true) => Ok(LittleEndian::read_int(val, size)),
322 (Endian::Big, true) => Ok(BigEndian::read_int(val, size)),
323 (Endian::Little, false) => i64::try_from(LittleEndian::read_uint(val, size)),
324 (Endian::Big, false) => i64::try_from(BigEndian::read_uint(val, size)),
325 };
326
327 match val {
328 Ok(val) => Value::int(val, head),
329 Err(_) => Value::error(
330 ShellError::IncorrectValue {
331 msg: "unsigned binary input is too large to convert to int".into(),
332 val_span,
333 call_span: head,
334 },
335 head,
336 ),
337 }
338 }
339 Value::Error { .. } => input.clone(),
341 other => Value::error(
342 ShellError::OnlySupportsThisInputType {
343 exp_input_type: "int, float, filesize, date, string, binary, duration, or bool"
344 .into(),
345 wrong_type: other.get_type().to_string(),
346 dst_span: head,
347 src_span: other.span(),
348 },
349 head,
350 ),
351 }
352}
353
354fn convert_int(input: &Value, head: Span, radix: u32) -> Value {
355 let i = match input {
356 Value::Int { val, .. } => val.to_string(),
357 Value::String { val, .. } => {
358 let val = val.trim();
359 if val.starts_with("0x") || val.starts_with("0b") || val.starts_with("0o")
362 {
364 match int_from_string(val, head) {
365 Ok(x) => return Value::int(x, head),
366 Err(e) => return Value::error(e, head),
367 }
368 } else if val.starts_with("00") {
369 match i64::from_str_radix(val, radix) {
371 Ok(n) => return Value::int(n, head),
372 Err(e) => {
373 return Value::error(
374 ShellError::CantConvert {
375 to_type: "string".to_string(),
376 from_type: "int".to_string(),
377 span: head,
378 help: Some(e.to_string()),
379 },
380 head,
381 );
382 }
383 }
384 }
385 val.to_string()
386 }
387 Value::Error { .. } => return input.clone(),
389 other => {
390 return Value::error(
391 ShellError::OnlySupportsThisInputType {
392 exp_input_type: "string and int".into(),
393 wrong_type: other.get_type().to_string(),
394 dst_span: head,
395 src_span: other.span(),
396 },
397 head,
398 );
399 }
400 };
401 match i64::from_str_radix(i.trim(), radix) {
402 Ok(n) => Value::int(n, head),
403 Err(_reason) => Value::error(
404 ShellError::CantConvert {
405 to_type: "string".to_string(),
406 from_type: "int".to_string(),
407 span: head,
408 help: None,
409 },
410 head,
411 ),
412 }
413}
414
415fn int_from_string(a_string: &str, span: Span) -> Result<i64, ShellError> {
416 let locale = get_system_locale();
418
419 let no_comma_string = a_string.replace(locale.separator(), "");
422
423 let trimmed = no_comma_string.trim();
424 match trimmed {
425 b if b.starts_with("0b") => {
426 let num = match i64::from_str_radix(b.trim_start_matches("0b"), 2) {
427 Ok(n) => n,
428 Err(_reason) => {
429 return Err(ShellError::CantConvert {
430 to_type: "int".to_string(),
431 from_type: "string".to_string(),
432 span,
433 help: Some(r#"digits following "0b" can only be 0 or 1"#.to_string()),
434 });
435 }
436 };
437 Ok(num)
438 }
439 h if h.starts_with("0x") => {
440 let num =
441 match i64::from_str_radix(h.trim_start_matches("0x"), 16) {
442 Ok(n) => n,
443 Err(_reason) => return Err(ShellError::CantConvert {
444 to_type: "int".to_string(),
445 from_type: "string".to_string(),
446 span,
447 help: Some(
448 r#"hexadecimal digits following "0x" should be in 0-9, a-f, or A-F"#
449 .to_string(),
450 ),
451 }),
452 };
453 Ok(num)
454 }
455 o if o.starts_with("0o") => {
456 let num = match i64::from_str_radix(o.trim_start_matches("0o"), 8) {
457 Ok(n) => n,
458 Err(_reason) => {
459 return Err(ShellError::CantConvert {
460 to_type: "int".to_string(),
461 from_type: "string".to_string(),
462 span,
463 help: Some(r#"octal digits following "0o" should be in 0-7"#.to_string()),
464 });
465 }
466 };
467 Ok(num)
468 }
469 _ => match trimmed.parse::<i64>() {
470 Ok(n) => Ok(n),
471 Err(_) => match a_string.parse::<f64>() {
472 Ok(f) => Ok(f as i64),
473 _ => Err(ShellError::CantConvert {
474 to_type: "int".to_string(),
475 from_type: "string".to_string(),
476 span,
477 help: Some(format!(
478 r#"string "{trimmed}" does not represent a valid integer"#
479 )),
480 }),
481 },
482 },
483 }
484}
485
486#[cfg(test)]
487mod test {
488 use chrono::{DateTime, FixedOffset};
489 use rstest::rstest;
490
491 use super::Value;
492 use super::*;
493 use nu_protocol::Type::Error;
494
495 #[test]
496 #[env(NU_TEST_LOCALE_OVERRIDE = "en_US.utf8")]
497 fn test_examples() -> nu_test_support::Result {
498 nu_test_support::test().examples(IntoInt)
499 }
500
501 #[test]
502 fn turns_to_integer() {
503 let word = Value::test_string("10");
504 let expected = Value::test_int(10);
505
506 let actual = action(
507 &word,
508 &Arguments {
509 radix: 10,
510 cell_paths: None,
511 signed: false,
512 endian: Endian::Big,
513 },
514 Span::test_data(),
515 );
516 assert_eq!(actual, expected);
517 }
518
519 #[test]
520 fn turns_binary_to_integer() {
521 let s = Value::test_string("0b101");
522 let actual = action(
523 &s,
524 &Arguments {
525 radix: 10,
526 cell_paths: None,
527 signed: false,
528 endian: Endian::Big,
529 },
530 Span::test_data(),
531 );
532 assert_eq!(actual, Value::test_int(5));
533 }
534
535 #[test]
536 fn turns_hex_to_integer() {
537 let s = Value::test_string("0xFF");
538 let actual = action(
539 &s,
540 &Arguments {
541 radix: 16,
542 cell_paths: None,
543 signed: false,
544 endian: Endian::Big,
545 },
546 Span::test_data(),
547 );
548 assert_eq!(actual, Value::test_int(255));
549 }
550
551 #[test]
552 fn communicates_parsing_error_given_an_invalid_integerlike_string() {
553 let integer_str = Value::test_string("36anra");
554
555 let actual = action(
556 &integer_str,
557 &Arguments {
558 radix: 10,
559 cell_paths: None,
560 signed: false,
561 endian: Endian::Big,
562 },
563 Span::test_data(),
564 );
565
566 assert_eq!(actual.get_type(), Error)
567 }
568
569 #[rstest]
570 #[case("2262-04-11T23:47:16+00:00", 0x7fff_ffff_ffff_ffff)]
571 #[case("1970-01-01T00:00:00+00:00", 0)]
572 #[case("1677-09-21T00:12:44+00:00", -0x7fff_ffff_ffff_ffff)]
573 fn datetime_to_int_values_that_work(
574 #[case] dt_in: DateTime<FixedOffset>,
575 #[case] int_expected: i64,
576 ) {
577 let s = Value::test_date(dt_in);
578 let actual = action(
579 &s,
580 &Arguments {
581 radix: 10,
582 cell_paths: None,
583 signed: false,
584 endian: Endian::Big,
585 },
586 Span::test_data(),
587 );
588 let exp_truncated = (int_expected / 1_000_000_000) * 1_000_000_000;
590 assert_eq!(actual, Value::test_int(exp_truncated));
591 }
592
593 #[rstest]
594 #[case("2262-04-11T23:47:17+00:00", "DateTime out of range for timestamp")]
595 #[case("1677-09-21T00:12:43+00:00", "DateTime out of range for timestamp")]
596 fn datetime_to_int_values_that_fail(
597 #[case] dt_in: DateTime<FixedOffset>,
598 #[case] err_expected: &str,
599 ) {
600 let s = Value::test_date(dt_in);
601 let actual = action(
602 &s,
603 &Arguments {
604 radix: 10,
605 cell_paths: None,
606 signed: false,
607 endian: Endian::Big,
608 },
609 Span::test_data(),
610 );
611 if let Value::Error { error, .. } = actual {
612 if let ShellError::IncorrectValue { msg: e, .. } = *error {
613 assert!(
614 e.contains(err_expected),
615 "{e:?} doesn't contain {err_expected}"
616 );
617 } else {
618 panic!("Unexpected error variant {error:?}")
619 }
620 } else {
621 panic!("Unexpected actual value {actual:?}")
622 }
623 }
624}