corvus_json_schema 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/Cargo.lock +390 -0
  3. data/Cargo.toml +8 -0
  4. data/LICENSE +201 -0
  5. data/README.md +111 -0
  6. data/VERSIONHISTORY.md +7 -0
  7. data/ext/corvus_json_schema/Cargo.toml +18 -0
  8. data/ext/corvus_json_schema/extconf.rb +6 -0
  9. data/ext/corvus_json_schema/rustfmt.toml +2 -0
  10. data/ext/corvus_json_schema/src/lib.rs +643 -0
  11. data/ext/corvus_json_schema/vendor/corvus-json-schema/Cargo.toml +35 -0
  12. data/ext/corvus_json_schema/vendor/corvus-json-schema/LICENSE +201 -0
  13. data/ext/corvus_json_schema/vendor/corvus-json-schema/README.md +139 -0
  14. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/compiler.rs +1118 -0
  15. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/dialect.rs +108 -0
  16. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/document.rs +905 -0
  17. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/eval/plan/fused.rs +1187 -0
  18. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/eval/plan.rs +1728 -0
  19. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/eval.rs +1451 -0
  20. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/formats.rs +1021 -0
  21. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/instance.rs +228 -0
  22. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/lib.rs +189 -0
  23. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/loader.rs +403 -0
  24. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/metaschemas.rs +29 -0
  25. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/node.rs +417 -0
  26. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/numbers.rs +244 -0
  27. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/options.rs +108 -0
  28. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/pattern.rs +1591 -0
  29. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/results.rs +316 -0
  30. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/uri.rs +274 -0
  31. data/lib/corvus_json_schema/version.rb +5 -0
  32. data/lib/corvus_json_schema.rb +65 -0
  33. metadata +94 -0
@@ -0,0 +1,417 @@
1
+ //! The compiled schema graph: one `SchemaNode` per distinct (document, pointer), with pre-digested keyword data.
2
+ //! A port of `Corvus.Text.Json.RuntimeEvaluator.Compilation.SchemaNode` (via the TypeScript port's `node.ts`).
3
+
4
+ use std::sync::Arc;
5
+
6
+ use serde_json::{Number, Value};
7
+
8
+ use crate::dialect::Dialect;
9
+ use crate::formats::FormatKind;
10
+ use crate::pattern::Pattern;
11
+
12
+ /// A node index in the compiled graph.
13
+ pub(crate) type NodeId = u32;
14
+
15
+ /// JSON types as bits.
16
+ pub(crate) mod type_mask {
17
+ pub const NULL: u8 = 1;
18
+ pub const BOOLEAN: u8 = 2;
19
+ pub const OBJECT: u8 = 4;
20
+ pub const ARRAY: u8 = 8;
21
+ pub const NUMBER: u8 = 16;
22
+ pub const STRING: u8 = 32;
23
+ pub const INTEGER: u8 = 64;
24
+ }
25
+
26
+ #[derive(Clone, Copy, PartialEq, Eq, Debug)]
27
+ pub(crate) enum ContentKind {
28
+ None,
29
+ Base64,
30
+ Json,
31
+ Base64Json,
32
+ }
33
+
34
+ /// An annotation-producing keyword and its value (reported in verbose results).
35
+ #[derive(Clone, Debug)]
36
+ pub(crate) struct AnnotationEntry {
37
+ pub keyword: String,
38
+ pub value: Value,
39
+ /// Reported only when the instance is a string (content keywords).
40
+ pub strings_only: bool,
41
+ }
42
+
43
+ #[derive(Clone, Debug)]
44
+ pub(crate) struct PatternProperty {
45
+ pub pattern: Arc<Pattern>,
46
+ pub node: NodeId,
47
+ }
48
+
49
+ /// The keyword a dependency entry came from, which names its result rows.
50
+ #[derive(Clone, Copy, PartialEq, Eq, Debug)]
51
+ pub(crate) enum DependencyKeyword {
52
+ Dependencies,
53
+ DependentSchemas,
54
+ DependentRequired,
55
+ }
56
+
57
+ impl DependencyKeyword {
58
+ pub fn name(self) -> &'static str {
59
+ match self {
60
+ DependencyKeyword::Dependencies => "dependencies",
61
+ DependencyKeyword::DependentSchemas => "dependentSchemas",
62
+ DependencyKeyword::DependentRequired => "dependentRequired",
63
+ }
64
+ }
65
+ }
66
+
67
+ #[derive(Clone, Debug)]
68
+ pub(crate) struct DependencyEntry {
69
+ pub keyword: DependencyKeyword,
70
+ pub name: String,
71
+ pub required: Option<Vec<String>>,
72
+ pub schema: Option<NodeId>,
73
+ }
74
+
75
+ /// A `$dynamicRef`/`$recursiveRef` that stays dynamic after compile-time analysis.
76
+ #[derive(Clone, Debug)]
77
+ pub(crate) struct DynamicRefTarget {
78
+ pub is_recursive: bool,
79
+ pub fallback: NodeId,
80
+ /// (resource id, node id of that resource's matching anchor), for the resources that define it.
81
+ pub by_resource: Vec<(u32, NodeId)>,
82
+ }
83
+
84
+ /// A discriminator value: the JSON scalars `const`/`enum` can name.
85
+ #[derive(Clone, Debug, PartialEq)]
86
+ pub(crate) enum DiscriminatorValue {
87
+ String(String),
88
+ Number(Number),
89
+ Bool(bool),
90
+ Null,
91
+ }
92
+
93
+ impl DiscriminatorValue {
94
+ /// Whether an instance value equals this one (numbers by value: `1` and `1.0` are the same key).
95
+ pub fn matches<'a, I: crate::instance::Instance<'a>>(&self, v: I) -> bool {
96
+ use crate::instance::View;
97
+ match (self, v.view()) {
98
+ (DiscriminatorValue::String(a), View::String(b)) => a == b,
99
+ (DiscriminatorValue::Number(a), View::Number(b)) => crate::numbers::num_eq(a, &b),
100
+ (DiscriminatorValue::Bool(a), View::Bool(b)) => *a == b,
101
+ (DiscriminatorValue::Null, View::Null) => true,
102
+ _ => false,
103
+ }
104
+ }
105
+
106
+ /// Key equality, with numbers by value.
107
+ pub fn same(&self, other: &DiscriminatorValue) -> bool {
108
+ match (self, other) {
109
+ (DiscriminatorValue::Number(a), DiscriminatorValue::Number(b)) => crate::numbers::num_eq(a, b),
110
+ _ => self == other,
111
+ }
112
+ }
113
+ }
114
+
115
+ /// Selects oneOf/anyOf branches by the value of one property (`SchemaCompiler.BuildDiscriminator`).
116
+ #[derive(Clone, Debug)]
117
+ pub(crate) struct Discriminator {
118
+ pub property: String,
119
+ /// Known discriminator values and the branches (indexes into the keyword's list) each can select.
120
+ pub known: Vec<(DiscriminatorValue, Vec<u32>)>,
121
+ /// Branches that stay candidates for a value not in `known` (negative and wildcard branches).
122
+ pub unknown: Vec<u32>,
123
+ /// Every branch requires the property, so its absence fails the keyword at once.
124
+ pub all_require: bool,
125
+ }
126
+
127
+ #[derive(Clone, Copy, PartialEq, Eq, Debug)]
128
+ pub(crate) enum StaticDynamicKeyword {
129
+ DynamicRef,
130
+ RecursiveRef,
131
+ }
132
+
133
+ impl StaticDynamicKeyword {
134
+ pub fn name(self) -> &'static str {
135
+ match self {
136
+ StaticDynamicKeyword::DynamicRef => "$dynamicRef",
137
+ StaticDynamicKeyword::RecursiveRef => "$recursiveRef",
138
+ }
139
+ }
140
+ }
141
+
142
+ #[derive(Debug)]
143
+ pub(crate) struct SchemaNode {
144
+ pub resource_id: u32,
145
+ pub dialect: Dialect,
146
+ /// The JSON pointer of the schema within its document (C#'s SchemaLocation).
147
+ pub pointer: String,
148
+
149
+ pub always_true: bool,
150
+ pub always_false: bool,
151
+
152
+ // Assertions.
153
+ pub type_mask: u8,
154
+ pub has_type: bool,
155
+ pub const_value: Option<Value>,
156
+ pub enum_values: Option<Vec<Value>>,
157
+
158
+ // References.
159
+ pub ref_: Option<NodeId>,
160
+ /// A `$dynamicRef`/`$recursiveRef` that compile-time analysis resolved statically, and its keyword.
161
+ pub static_dynamic_ref: Option<NodeId>,
162
+ pub static_dynamic_keyword: StaticDynamicKeyword,
163
+ pub dynamic_ref: Option<Box<DynamicRefTarget>>,
164
+
165
+ // In-place applicators.
166
+ pub all_of: Option<Vec<NodeId>>,
167
+ pub any_of: Option<Vec<NodeId>>,
168
+ pub one_of: Option<Vec<NodeId>>,
169
+ pub not: Option<NodeId>,
170
+ pub if_: Option<NodeId>,
171
+ pub then: Option<NodeId>,
172
+ pub else_: Option<NodeId>,
173
+
174
+ // Objects.
175
+ pub properties: Option<Vec<(String, NodeId)>>,
176
+ pub pattern_properties: Option<Vec<PatternProperty>>,
177
+ pub additional_properties: Option<NodeId>,
178
+ pub property_names: Option<NodeId>,
179
+ /// `required` without duplicates.
180
+ pub required: Option<Vec<String>>,
181
+ /// `required` as written (duplicates kept), for results.
182
+ pub required_list: Option<Vec<String>>,
183
+ pub dependencies: Option<Vec<DependencyEntry>>,
184
+ pub min_properties: Option<u64>,
185
+ pub max_properties: Option<u64>,
186
+ pub unevaluated_properties: Option<NodeId>,
187
+
188
+ // Arrays.
189
+ pub prefix_items: Option<Vec<NodeId>>,
190
+ /// The keywords behind prefix_items/items: `prefixItems`/`items` (2020-12) or `items`/`additionalItems` (legacy).
191
+ pub prefix_keyword: &'static str,
192
+ pub items_keyword: &'static str,
193
+ pub items: Option<NodeId>,
194
+ pub contains: Option<NodeId>,
195
+ pub min_contains: u64,
196
+ pub max_contains: Option<u64>,
197
+ pub contains_marks_evaluated: bool,
198
+ pub min_items: Option<u64>,
199
+ pub max_items: Option<u64>,
200
+ pub unique_items: bool,
201
+ pub unevaluated_items: Option<NodeId>,
202
+
203
+ // Strings.
204
+ pub min_length: Option<u64>,
205
+ pub max_length: Option<u64>,
206
+ pub pattern: Option<Arc<Pattern>>,
207
+ pub format: Option<String>,
208
+ pub format_kind: FormatKind,
209
+ pub assert_format: bool,
210
+ pub content: ContentKind,
211
+ pub assert_content: bool,
212
+
213
+ // Numbers.
214
+ pub minimum: Option<Number>,
215
+ pub maximum: Option<Number>,
216
+ pub exclusive_minimum: Option<Number>,
217
+ pub exclusive_maximum: Option<Number>,
218
+ pub multiple_of: Option<Number>,
219
+
220
+ // Analysis.
221
+ pub marks_properties: bool,
222
+ pub marks_items: bool,
223
+ pub in_place_cycle: bool,
224
+ pub one_of_discriminator: Option<Box<Discriminator>>,
225
+ pub any_of_discriminator: Option<Box<Discriminator>>,
226
+ }
227
+
228
+ impl SchemaNode {
229
+ pub fn new(resource_id: u32, dialect: Dialect, pointer: String) -> SchemaNode {
230
+ SchemaNode {
231
+ resource_id,
232
+ dialect,
233
+ pointer,
234
+ always_true: false,
235
+ always_false: false,
236
+ type_mask: 0,
237
+ has_type: false,
238
+ const_value: None,
239
+ enum_values: None,
240
+ ref_: None,
241
+ static_dynamic_ref: None,
242
+ static_dynamic_keyword: StaticDynamicKeyword::DynamicRef,
243
+ dynamic_ref: None,
244
+ all_of: None,
245
+ any_of: None,
246
+ one_of: None,
247
+ not: None,
248
+ if_: None,
249
+ then: None,
250
+ else_: None,
251
+ properties: None,
252
+ pattern_properties: None,
253
+ additional_properties: None,
254
+ property_names: None,
255
+ required: None,
256
+ required_list: None,
257
+ dependencies: None,
258
+ min_properties: None,
259
+ max_properties: None,
260
+ unevaluated_properties: None,
261
+ prefix_items: None,
262
+ prefix_keyword: "prefixItems",
263
+ items_keyword: "items",
264
+ items: None,
265
+ contains: None,
266
+ min_contains: 1,
267
+ max_contains: None,
268
+ contains_marks_evaluated: false,
269
+ min_items: None,
270
+ max_items: None,
271
+ unique_items: false,
272
+ unevaluated_items: None,
273
+ min_length: None,
274
+ max_length: None,
275
+ pattern: None,
276
+ format: None,
277
+ format_kind: FormatKind::Unknown,
278
+ assert_format: false,
279
+ content: ContentKind::None,
280
+ assert_content: false,
281
+ minimum: None,
282
+ maximum: None,
283
+ exclusive_minimum: None,
284
+ exclusive_maximum: None,
285
+ multiple_of: None,
286
+ marks_properties: false,
287
+ marks_items: false,
288
+ in_place_cycle: false,
289
+ one_of_discriminator: None,
290
+ any_of_discriminator: None,
291
+ }
292
+ }
293
+
294
+ /// Keywords that apply only to objects.
295
+ pub fn has_object_keywords(&self) -> bool {
296
+ self.properties.is_some()
297
+ || self.pattern_properties.is_some()
298
+ || self.additional_properties.is_some()
299
+ || self.property_names.is_some()
300
+ || self.required.is_some()
301
+ || self.dependencies.is_some()
302
+ || self.min_properties.is_some()
303
+ || self.max_properties.is_some()
304
+ || self.unevaluated_properties.is_some()
305
+ }
306
+
307
+ pub fn has_array_keywords(&self) -> bool {
308
+ self.prefix_items.is_some()
309
+ || self.items.is_some()
310
+ || self.contains.is_some()
311
+ || self.min_items.is_some()
312
+ || self.max_items.is_some()
313
+ || self.unique_items
314
+ || self.unevaluated_items.is_some()
315
+ }
316
+
317
+ pub fn has_string_keywords(&self) -> bool {
318
+ self.min_length.is_some()
319
+ || self.max_length.is_some()
320
+ || self.pattern.is_some()
321
+ || (self.assert_format && self.format.is_some() && !self.format_kind.is_numeric())
322
+ || self.assert_content
323
+ }
324
+
325
+ pub fn has_number_keywords(&self) -> bool {
326
+ self.minimum.is_some()
327
+ || self.maximum.is_some()
328
+ || self.exclusive_minimum.is_some()
329
+ || self.exclusive_maximum.is_some()
330
+ || self.multiple_of.is_some()
331
+ || (self.assert_format && self.format.is_some() && self.format_kind.is_numeric())
332
+ }
333
+
334
+ pub fn has_in_place_applicators(&self) -> bool {
335
+ self.ref_.is_some()
336
+ || self.static_dynamic_ref.is_some()
337
+ || self.dynamic_ref.is_some()
338
+ || self.all_of.is_some()
339
+ || self.any_of.is_some()
340
+ || self.one_of.is_some()
341
+ || self.not.is_some()
342
+ || self.if_.is_some()
343
+ || self.dependencies.as_ref().is_some_and(|d| d.iter().any(|e| e.schema.is_some()))
344
+ }
345
+
346
+ /// Nothing but `$ref`: no other keyword that asserts.
347
+ pub fn is_pure_ref(&self) -> bool {
348
+ self.ref_.is_some()
349
+ && !self.has_type
350
+ && self.const_value.is_none()
351
+ && self.enum_values.is_none()
352
+ && !self.has_object_keywords()
353
+ && !self.has_array_keywords()
354
+ && !self.has_string_keywords()
355
+ && !self.has_number_keywords()
356
+ && self.dynamic_ref.is_none()
357
+ && self.static_dynamic_ref.is_none()
358
+ && self.all_of.is_none()
359
+ && self.any_of.is_none()
360
+ && self.one_of.is_none()
361
+ && self.not.is_none()
362
+ && self.if_.is_none()
363
+ && self.dependencies.is_none()
364
+ }
365
+
366
+ /// In-place children (the instance is evaluated at the same location).
367
+ pub fn in_place_children(&self, include_not: bool) -> Vec<NodeId> {
368
+ let mut out = Vec::new();
369
+ out.extend(self.ref_);
370
+ out.extend(self.static_dynamic_ref);
371
+ if let Some(d) = &self.dynamic_ref {
372
+ out.push(d.fallback);
373
+ out.extend(d.by_resource.iter().map(|&(_, n)| n));
374
+ }
375
+ for list in [&self.all_of, &self.any_of, &self.one_of].into_iter().flatten() {
376
+ out.extend_from_slice(list);
377
+ }
378
+ if include_not {
379
+ out.extend(self.not);
380
+ }
381
+ out.extend(self.if_);
382
+ out.extend(self.then);
383
+ out.extend(self.else_);
384
+ if let Some(deps) = &self.dependencies {
385
+ out.extend(deps.iter().filter_map(|d| d.schema));
386
+ }
387
+ out
388
+ }
389
+
390
+ /// Every child node.
391
+ pub fn children(&self) -> Vec<NodeId> {
392
+ let mut out = self.in_place_children(true);
393
+ if let Some(p) = &self.properties {
394
+ out.extend(p.iter().map(|&(_, n)| n));
395
+ }
396
+ if let Some(p) = &self.pattern_properties {
397
+ out.extend(p.iter().map(|pp| pp.node));
398
+ }
399
+ for c in [
400
+ self.additional_properties,
401
+ self.property_names,
402
+ self.unevaluated_properties,
403
+ self.items,
404
+ self.contains,
405
+ self.unevaluated_items,
406
+ ]
407
+ .into_iter()
408
+ .flatten()
409
+ {
410
+ out.push(c);
411
+ }
412
+ if let Some(p) = &self.prefix_items {
413
+ out.extend_from_slice(p);
414
+ }
415
+ out
416
+ }
417
+ }
@@ -0,0 +1,244 @@
1
+ //! Exact numeric comparisons over `serde_json::Number` (integers stay exact; floats compare by value), and exact
2
+ //! `multipleOf` over decimal forms, matching the C# evaluator's normalised-decimal semantics.
3
+
4
+ use std::cmp::Ordering;
5
+
6
+ use serde_json::Number;
7
+
8
+ /// The value of a JSON number: an exact integer, or a double.
9
+ #[derive(Clone, Copy, Debug)]
10
+ pub(crate) enum Num {
11
+ I(i128),
12
+ F(f64),
13
+ }
14
+
15
+ impl Num {
16
+ #[inline]
17
+ pub fn of(n: &Number) -> Num {
18
+ if let Some(i) = n.as_i64() {
19
+ Num::I(i as i128)
20
+ } else if let Some(u) = n.as_u64() {
21
+ Num::I(u as i128)
22
+ } else {
23
+ let f = n.as_f64().unwrap_or(f64::NAN);
24
+ // A double that holds an integer exactly (1.0, 1e3) is that integer.
25
+ if f.fract() == 0.0 && f.abs() < 1.0e36 { Num::I(f as i128) } else { Num::F(f) }
26
+ }
27
+ }
28
+ }
29
+
30
+ /// Compares an integer with a double exactly.
31
+ fn cmp_int_float(i: i128, f: f64) -> Ordering {
32
+ if f.is_nan() {
33
+ return Ordering::Less;
34
+ }
35
+ if f >= 1.7e38 {
36
+ return Ordering::Less;
37
+ }
38
+ if f <= -1.7e38 {
39
+ return Ordering::Greater;
40
+ }
41
+ let fl = f.floor();
42
+ let fi = fl as i128;
43
+ match i.cmp(&fi) {
44
+ Ordering::Equal => {
45
+ if f > fl {
46
+ Ordering::Less
47
+ } else {
48
+ Ordering::Equal
49
+ }
50
+ }
51
+ o => o,
52
+ }
53
+ }
54
+
55
+ #[inline]
56
+ pub(crate) fn cmp(a: Num, b: Num) -> Ordering {
57
+ match (a, b) {
58
+ (Num::I(x), Num::I(y)) => x.cmp(&y),
59
+ (Num::I(x), Num::F(y)) => cmp_int_float(x, y),
60
+ (Num::F(x), Num::I(y)) => cmp_int_float(y, x).reverse(),
61
+ (Num::F(x), Num::F(y)) => x.partial_cmp(&y).unwrap_or(Ordering::Equal),
62
+ }
63
+ }
64
+
65
+ /// JSON number equality (1 == 1.0).
66
+ #[inline]
67
+ pub(crate) fn num_eq(a: &Number, b: &Number) -> bool {
68
+ cmp(Num::of(a), Num::of(b)) == Ordering::Equal
69
+ }
70
+
71
+ /// A decimal: mantissa × 10^exponent, from the shortest round-trip text of the number.
72
+ fn decimal(n: Num) -> Option<(i128, i32)> {
73
+ match n {
74
+ Num::I(i) => Some((i, 0)),
75
+ Num::F(f) => {
76
+ if !f.is_finite() {
77
+ return None;
78
+ }
79
+ // `{:e}` gives the shortest round-trip digits: "1.25e-3", "5e0". Formatted on the stack.
80
+ let mut buf = StackText::default();
81
+ std::fmt::Write::write_fmt(&mut buf, format_args!("{f:e}")).ok()?;
82
+ let s = buf.as_str();
83
+ let (mant, exp) = s.split_once('e')?;
84
+ let exp: i32 = exp.parse().ok()?;
85
+ let neg = mant.starts_with('-');
86
+ let mant = mant.trim_start_matches('-');
87
+ let (int_part, frac) = mant.split_once('.').unwrap_or((mant, ""));
88
+ if int_part.len() + frac.len() > 36 {
89
+ return None;
90
+ }
91
+ let mut m: i128 = 0;
92
+ for c in int_part.bytes().chain(frac.bytes()) {
93
+ m = m * 10 + (c - b'0') as i128;
94
+ }
95
+ Some((if neg { -m } else { m }, exp - frac.len() as i32))
96
+ }
97
+ }
98
+ }
99
+
100
+ /// A short text formatted without allocating (a double's `{:e}` form is at most 24 bytes).
101
+ struct StackText {
102
+ buf: [u8; 40],
103
+ len: usize,
104
+ }
105
+
106
+ impl Default for StackText {
107
+ fn default() -> Self {
108
+ StackText { buf: [0; 40], len: 0 }
109
+ }
110
+ }
111
+
112
+ impl StackText {
113
+ fn as_str(&self) -> &str {
114
+ std::str::from_utf8(&self.buf[..self.len]).unwrap_or("")
115
+ }
116
+ }
117
+
118
+ impl std::fmt::Write for StackText {
119
+ fn write_str(&mut self, s: &str) -> std::fmt::Result {
120
+ let end = self.len + s.len();
121
+ if end > self.buf.len() {
122
+ return Err(std::fmt::Error);
123
+ }
124
+ self.buf[self.len..end].copy_from_slice(s.as_bytes());
125
+ self.len = end;
126
+ Ok(())
127
+ }
128
+ }
129
+
130
+ fn gcd(mut a: u128, mut b: u128) -> u128 {
131
+ while b != 0 {
132
+ (a, b) = (b, a % b);
133
+ }
134
+ a
135
+ }
136
+
137
+ /// Whether `m * 10^shift` is divisible by `d` (`d > 0`), without forming the (possibly huge) product: after removing
138
+ /// the common factor of `m` and `d`, what remains of `d` must divide `10^shift`, so hold only 2s and 5s.
139
+ fn divides_scaled(m: u128, shift: u32, d: u128) -> bool {
140
+ let mut rest = d / gcd(m, d);
141
+ for f in [2u128, 5] {
142
+ let mut count = 0u32;
143
+ while rest % f == 0 {
144
+ rest /= f;
145
+ count += 1;
146
+ }
147
+ if count > shift {
148
+ return false;
149
+ }
150
+ }
151
+ rest == 1
152
+ }
153
+
154
+ /// A `multipleOf` divisor with its integer or decimal form worked out once (the C# evaluator's DivisorValue).
155
+ #[derive(Clone, Debug)]
156
+ pub(crate) struct Divisor {
157
+ int: Option<i128>,
158
+ /// Magnitude of the mantissa and the exponent: the divisor is `m * 10^e`.
159
+ decimal: Option<(u128, i32)>,
160
+ }
161
+
162
+ impl Divisor {
163
+ pub fn new(d: &Number) -> Divisor {
164
+ let n = Num::of(d);
165
+ Divisor {
166
+ int: match n {
167
+ Num::I(i) => Some(i),
168
+ Num::F(_) => None,
169
+ },
170
+ decimal: decimal(n).map(|(m, e)| (m.unsigned_abs(), e)),
171
+ }
172
+ }
173
+
174
+ /// Exact `multipleOf`: whether `x / d` is an integer, over the decimal forms of both numbers.
175
+ pub fn divides(&self, x: &Number) -> bool {
176
+ let xn = Num::of(x);
177
+ if let (Num::I(a), Some(b)) = (xn, self.int) {
178
+ return b != 0 && a % b == 0;
179
+ }
180
+ let (Some((am, ae)), Some((bm, be))) = (decimal(xn), self.decimal) else {
181
+ return false;
182
+ };
183
+ let am = am.unsigned_abs();
184
+ if bm == 0 {
185
+ return false;
186
+ }
187
+ if am == 0 {
188
+ return true;
189
+ }
190
+ let shift = ae - be;
191
+ if shift >= 0 {
192
+ divides_scaled(am, shift as u32, bm)
193
+ } else {
194
+ // am must be divisible by bm * 10^-shift; am < 10^37, so a larger power of ten cannot divide it.
195
+ match 10u128.checked_pow((-shift) as u32).and_then(|p| bm.checked_mul(p)) {
196
+ Some(den) => am % den == 0,
197
+ None => false,
198
+ }
199
+ }
200
+ }
201
+ }
202
+
203
+ /// Exact `multipleOf`: whether `x / d` is an integer, over the decimal forms of both numbers.
204
+ pub(crate) fn multiple_of(x: &Number, d: &Number) -> bool {
205
+ Divisor::new(d).divides(x)
206
+ }
207
+
208
+ /// The number's text as C# formats it in messages.
209
+ pub(crate) fn number_text(n: &Number) -> String {
210
+ n.to_string()
211
+ }
212
+
213
+ #[cfg(test)]
214
+ mod tests {
215
+ use super::*;
216
+ use serde_json::json;
217
+
218
+ fn n(v: serde_json::Value) -> Number {
219
+ v.as_number().unwrap().clone()
220
+ }
221
+
222
+ #[test]
223
+ fn multiple_of_is_exact() {
224
+ assert!(multiple_of(&n(json!(0.0075)), &n(json!(0.0001))));
225
+ assert!(!multiple_of(&n(json!(0.00751)), &n(json!(0.0001))));
226
+ assert!(multiple_of(&n(json!(4.5)), &n(json!(1.5))));
227
+ assert!(!multiple_of(&n(json!(35)), &n(json!(1.5))));
228
+ assert!(multiple_of(&n(json!(10)), &n(json!(5))));
229
+ assert!(!multiple_of(&n(json!(1e308)), &n(json!(0.123456789))));
230
+ assert!(multiple_of(&n(json!(1e308)), &n(json!(0.5))));
231
+ assert!(multiple_of(&n(json!(0)), &n(json!(0.3))));
232
+ assert!(!multiple_of(&n(json!(1e-300)), &n(json!(1e-7))));
233
+ }
234
+
235
+ #[test]
236
+ fn comparisons_are_exact() {
237
+ assert_eq!(
238
+ cmp(Num::of(&n(json!(9007199254740993u64))), Num::of(&n(json!(9007199254740992.0)))),
239
+ Ordering::Greater
240
+ );
241
+ assert_eq!(cmp(Num::of(&n(json!(1))), Num::of(&n(json!(1.0)))), Ordering::Equal);
242
+ assert_eq!(cmp(Num::of(&n(json!(-1))), Num::of(&n(json!(-0.5)))), Ordering::Less);
243
+ }
244
+ }