Skip to content
This repository was archived by the owner on May 27, 2026. It is now read-only.

Commit d93ec62

Browse files
authored
Merge pull request #36 from talmakion/feature/T6622/numeric-validator-radix-aware
validators: T6622: Radix-aware numeric validation
2 parents 5136180 + 8881e88 commit d93ec62

1 file changed

Lines changed: 67 additions & 21 deletions

File tree

src/validators/numeric.ml

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ type options = {
1111
relative: bool;
1212
allow_range: bool;
1313
require_range: bool;
14+
parse_hex: bool;
15+
parse_oct: bool;
16+
parse_bin: bool;
17+
parse_dec: bool;
1418
}
1519

1620
let default_opts = {
@@ -23,6 +27,10 @@ let default_opts = {
2327
relative = false;
2428
allow_range = false;
2529
require_range = false;
30+
parse_hex = false;
31+
parse_oct = false;
32+
parse_bin = false;
33+
parse_dec = false;
2634
}
2735

2836
let opts = ref default_opts
@@ -39,6 +47,10 @@ let args = [
3947
("--relative", Arg.Unit (fun () -> opts := {!opts with relative=true}), "Allow relative increment/decrement (+/-N)");
4048
("--allow-range", Arg.Unit (fun () -> opts := {!opts with allow_range=true}), "Allow the argument to be a range rather than a single number");
4149
("--require-range", Arg.Unit (fun () -> opts := {!opts with require_range=true; allow_range=true}), "Require the argument to be a range rather than a single number");
50+
("--hex", Arg.Unit (fun () -> opts := {!opts with parse_hex=true}), "Parse hexadecimal integers as valid numbers, complete with 0x-prefix");
51+
("--octal", Arg.Unit (fun () -> opts := {!opts with parse_oct=true}), "Parse octal integers as valid numbers, complete with 0o-prefix");
52+
("--binary", Arg.Unit (fun () -> opts := {!opts with parse_bin=true}), "Parse binary integers as valid numbers, complete with 0b-prefix");
53+
("--decimal", Arg.Unit (fun () -> opts := {!opts with parse_dec=true}), "Continue to parse decimal numbers even when other radixes are requested, no prefix required");
4254
("--", Arg.Rest (fun s -> number_arg := s), "Interpret next item as an argument");
4355
]
4456
let usage = Printf.sprintf "Usage: %s [OPTIONS] <number>|<range>" Sys.argv.(0)
@@ -64,12 +76,24 @@ let check_positive opts m =
6476
| Range_float _ ->
6577
failwith "option '--positive does' not apply to a range value"
6678

67-
let looks_like_number value =
79+
let looks_like_decimal value =
6880
try let _ = Pcre2.exec ~pat:"^(\\-?)[0-9]+(\\.[0-9]+)?$" value in true
6981
with Not_found -> false
7082

83+
let looks_like_hex value =
84+
try let _ = Pcre2.exec ~pat:"^(\\-?)0[xX][0-9a-fA-F]+$" value in true
85+
with Not_found -> false
86+
87+
let looks_like_octal value =
88+
try let _ = Pcre2.exec ~pat:"^(\\-?)0[oO][0-7]+$" value in true
89+
with Not_found -> false
90+
91+
let looks_like_binary value =
92+
try let _ = Pcre2.exec ~pat:"^(\\-?)0[bB][0-1]+$" value in true
93+
with Not_found -> false
94+
7195
let is_relative value =
72-
try let _ = Pcre2.exec ~pat:"^[+-][0-9]+$" value in true
96+
try let _ = Pcre2.exec ~pat:"^[+-](0[xboXBO])?[0-9a-fA-F]+$" value in true
7397
with Not_found -> false
7498

7599
let number_string_drop_modifier value =
@@ -87,24 +111,40 @@ let get_relative opts t =
87111
else t
88112

89113
let number_of_string opts s =
90-
if not (looks_like_number s) then Printf.ksprintf failwith "'%s' is not a valid number" s else
91-
let n = float_of_string_opt s in
92-
match n with
93-
| Some n ->
94-
(* If floats are explicitly allowed, just return the number. *)
95-
if opts.allow_float then n
96-
(* If floats are not explicitly allowed, check if the argument has a decimal separator in it.
97-
If the argument string contains a dot but float_of_string didn't dislike it,
98-
it's a valid number but not an integer.
99-
*)
100-
else if not (String.contains s '.') then n
101-
(* If float_of_string returned None, the argument string is just garbage rather than a number. *)
102-
else Printf.ksprintf failwith "'%s' is not a valid integer number" s
103-
| None ->
104-
Printf.ksprintf failwith "'%s' is not a valid number" s
114+
if (opts.allow_float && not opts.parse_dec) then
115+
failwith "Only decimal numbers may be floating point"
116+
else if (opts.parse_hex && (looks_like_hex s)) ||
117+
(opts.parse_oct && (looks_like_octal s)) ||
118+
(opts.parse_bin && (looks_like_binary s)) then
119+
(* float_of_string won't deal with octal or binary and hex-floats are just weird.
120+
Easier to separate non-decimal parsing this way.
121+
*)
122+
let n = int_of_string_opt s in
123+
match n with
124+
| Some n ->
125+
float_of_int n
126+
| None ->
127+
Printf.ksprintf failwith "'%s' is not a valid non-decimal number" s
128+
else if (opts.parse_dec && (looks_like_decimal s)) then
129+
let n = float_of_string_opt s in
130+
match n with
131+
| Some n ->
132+
(* If floats are explicitly allowed, just return the number. *)
133+
if opts.allow_float then n
134+
(* If floats are not explicitly allowed, check if the argument has a decimal separator in it.
135+
If the argument string contains a dot but float_of_string didn't dislike it,
136+
it's a valid number but not an integer.
137+
*)
138+
else if not (String.contains s '.') then n
139+
(* If float_of_string returned None, the argument string is just garbage rather than a number. *)
140+
else Printf.ksprintf failwith "'%s' is not a valid integer number" s
141+
| None ->
142+
Printf.ksprintf failwith "'%s' is not a valid number" s
143+
else Printf.ksprintf failwith "'%s' is not a valid number" s
105144

106145
let range_of_string opts s =
107-
let rs = String.split_on_char '-' s |> List.map String.trim |> List.map (number_of_string opts) in
146+
let param_opts = { opts with parse_dec = true } in
147+
let rs = String.split_on_char '-' s |> List.map String.trim |> List.map (number_of_string param_opts) in
108148
match rs with
109149
| [l; r] -> (l, r)
110150
| exception (Failure msg) ->
@@ -148,7 +188,8 @@ let check_not_ranges opts m =
148188
Printf.ksprintf failwith "Range is in one of excluded ranges"
149189

150190
let check_not_values opts m =
151-
let excluded_values = List.map (number_of_string opts) opts.not_values in
191+
let param_opts = { opts with parse_dec = true } in
192+
let excluded_values = List.map (number_of_string param_opts) opts.not_values in
152193
if excluded_values = [] then () else
153194
match m with
154195
| Range_float _ -> Printf.ksprintf failwith "--not-value cannot be used with ranges"
@@ -170,17 +211,22 @@ let check_argument_type opts m =
170211
else Printf.ksprintf failwith "Value must be a number, not a range"
171212

172213
let is_range_val s =
173-
try let _ = Pcre2.exec ~pat:"^[0-9]+-[0-9]+$" s in true
214+
try let _ = Pcre2.exec ~pat:"^(0[xboXBO])?[0-9a-fA-F]+-(0[xboXBO])?[0-9a-fA-F]+$" s in true
174215
with Not_found -> false
175216

176217
let var_numeric_str s =
177218
match is_range_val s with
178219
| true -> Range_string s
179220
| false -> Number_string s
180221

222+
let check_default_radix opts =
223+
if (not opts.parse_hex && not opts.parse_oct && not opts.parse_bin) then
224+
{opts with parse_dec=true}
225+
else opts
226+
181227
let () = try
182228
let s = var_numeric_str !number_arg in
183-
let opts = !opts in
229+
let opts = check_default_radix !opts in
184230
let s = get_relative opts s in
185231
let n =
186232
match s with

0 commit comments

Comments
 (0)