Skip to content

Commit 1990fba

Browse files
committed
Implement *, /, % for ion jq
Also documentation improvements etc. `*` is not fully-implemented, not compliant with `jq` standard.
1 parent 6e9479e commit 1990fba

File tree

1 file changed

+214
-32
lines changed
  • src/bin/ion/commands

1 file changed

+214
-32
lines changed

src/bin/ion/commands/jq.rs

Lines changed: 214 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
use crate::commands::jq::ion_math::DecimalMath;
12
use crate::commands::{CommandIo, IonCliCommand, WithIonCliArgument};
23
use crate::input::CommandInput;
34
use crate::output::{CommandOutput, CommandOutputWriter};
45
use anyhow::bail;
6+
use bigdecimal::ToPrimitive;
57
use clap::{arg, ArgMatches, Command};
68
use ion_rs::{
79
AnyEncoding, Element, ElementReader, IonData, IonType, List, Reader, Sequence, Value,
@@ -217,10 +219,29 @@ impl PartialOrd for JaqElement {
217219
impl Add for JaqElement {
218220
type Output = ValR<Self>;
219221

222+
/// From: https://jqlang.org/manual/#addition
223+
///
224+
/// > The operator `+` takes two filters, applies them both to the same input, and adds the
225+
/// results together. What "adding" means depends on the types involved:
226+
/// >
227+
/// > - Numbers are added by normal arithmetic.
228+
/// >
229+
/// > - Arrays are added by being concatenated into a larger array.
230+
/// >
231+
/// > - Strings are added by being joined into a larger string.
232+
/// >
233+
/// > - Objects are added by merging, that is, inserting all the key-value pairs from both
234+
/// > objects into a single combined object. If both objects contain a value for the same key,
235+
/// > the object on the right of the `+` wins. (For recursive merge use the `*` operator.)
236+
/// >
237+
/// > `null` can be added to any value, and returns the other value unchanged.
238+
///
239+
/// For Ion values we have slightly different semantics–we haven't yet implemented the
240+
/// overriding and deduplicating of keys for structs, so structs are simply merged
220241
fn add(self, _rhs: Self) -> Self::Output {
221242
let (lhv, rhv) = (self.into_value(), _rhs.into_value());
222243

223-
use ion_math::{DecimalMath, FloatMath};
244+
use ion_math::{DecimalMath, ToFloat};
224245
use Value::*;
225246

226247
let elt: Element = match (lhv, rhv) {
@@ -235,8 +256,15 @@ impl Add for JaqElement {
235256
(List(a), List(b)) => ion_rs::List::from_iter(a.into_iter().chain(b)).into(),
236257
(SExp(a), SExp(b)) => ion_rs::SExp::from_iter(a.into_iter().chain(b)).into(),
237258
(String(a), String(b)) => format!("{}{}", a.text(), b.text()).into(),
259+
(Symbol(a), Symbol(b)) => match (a.text(), b.text()) {
260+
(Some(ta), Some(tb)) => format!("{}{}", ta, tb),
261+
//TODO: Handle symbols with unknown text?
262+
_ => return jaq_binary_error(Symbol(a), Symbol(b), "cannot be added"),
263+
}
264+
.into(),
265+
238266
// Structs merge
239-
//TODO: Recursively remove duplicate fields, first field position but b and last field wins
267+
//TODO: Recursively remove duplicate fields, see doc comment for method
240268
(Struct(a), Struct(b)) => a.clone_builder().with_fields(b.fields()).build().into(),
241269

242270
// Number types, only lossless operations
@@ -246,16 +274,13 @@ impl Add for JaqElement {
246274
(Decimal(a), Int(b)) | (Int(b), Decimal(a)) => a.add(b).into(),
247275

248276
// Only try potentially lossy Float conversions when we've run out of the other options
249-
(b, Float(a)) | (Float(a), b)
250-
if matches!(b.ion_type(), IonType::Int | IonType::Decimal) =>
251-
{
252-
let Some(f) = b.clone().to_f64() else {
253-
return jaq_unary_error(b, "cannot be an f64");
277+
(a @ Int(_) | a @ Decimal(_), Float(b)) | (Float(b), a @ Int(_) | a @ Decimal(_)) => {
278+
let Some(f) = a.clone().to_f64() else {
279+
return jaq_unary_error(a, "cannot be an f64");
254280
};
255-
(f + a).into()
281+
(f + b).into()
256282
}
257283

258-
// Note this includes timestamps
259284
(a, b) => return jaq_binary_error(a, b, "cannot be added"),
260285
};
261286

@@ -266,10 +291,14 @@ impl Add for JaqElement {
266291
impl Sub for JaqElement {
267292
type Output = ValR<Self>;
268293

294+
/// From: https://jqlang.org/manual/#subtraction
295+
///
296+
/// > As well as normal arithmetic subtraction on numbers, the `-` operator can be used on
297+
/// > arrays to remove all occurrences of the second array's elements from the first array.
269298
fn sub(self, _rhs: Self) -> Self::Output {
270299
let (lhv, rhv) = (self.into_value(), _rhs.into_value());
271300

272-
use ion_math::{DecimalMath, FloatMath};
301+
use ion_math::{DecimalMath, ToFloat};
273302
use Value::*;
274303

275304
let elt: Element = match (lhv, rhv) {
@@ -287,7 +316,7 @@ impl Sub for JaqElement {
287316
}
288317

289318
// Number types, only lossless operations
290-
(Int(a), Int(b)) => (a + -b).into(), //TODO: use bare - when Int implements Sub
319+
(Int(a), Int(b)) => (a + -b).into(), //TODO: use bare - with ion-rs > rc.11
291320
(Float(a), Float(b)) => (a - b).into(),
292321
(Decimal(a), Decimal(b)) => a.sub(b).into(),
293322
(Decimal(a), Int(b)) => a.sub(b).into(),
@@ -307,7 +336,6 @@ impl Sub for JaqElement {
307336
(a - f).into()
308337
}
309338

310-
// Note this includes timestamps, strings, structs, and all nulls
311339
(a, b) => return jaq_binary_error(a, b, "cannot be subtracted"),
312340
};
313341

@@ -318,32 +346,172 @@ impl Sub for JaqElement {
318346
impl Mul for JaqElement {
319347
type Output = ValR<Self>;
320348

349+
/// From: https://jqlang.org/manual/#multiplication-division-modulo
350+
///
351+
/// > - Multiplying a string by a number produces the concatenation of that string that many times.
352+
/// > `"x" * 0` produces `""`.
353+
/// >
354+
/// > - Multiplying two objects will merge them recursively: this works like addition but if both
355+
/// > objects contain a value for the same key, and the values are objects, the two are merged
356+
/// > with the same strategy.
321357
fn mul(self, _rhs: Self) -> Self::Output {
322-
todo!()
358+
let (lhv, rhv) = (self.into_value(), _rhs.into_value());
359+
360+
use ion_math::{DecimalMath, ToFloat};
361+
use Value::*;
362+
363+
let elt: Element = match (lhv, rhv) {
364+
(String(a), Int(b)) | (Int(b), String(a)) => match b.as_usize() {
365+
Some(n) => a.text().repeat(n).into(),
366+
None => Null(IonType::Null).into(),
367+
},
368+
369+
(Symbol(a), Int(b)) | (Int(b), Symbol(a)) => match (b.as_usize(), a.text()) {
370+
(Some(n), Some(t)) => t.repeat(n).into(),
371+
//TODO: Handle symbols with unknown text?
372+
_ => Null(IonType::Null).into(),
373+
},
374+
375+
// Structs merge recursively
376+
//TODO: Recursively remove duplicate fields, recursively merge if struct fields collide
377+
(Struct(a), Struct(b)) => a.clone_builder().with_fields(b.fields()).build().into(),
378+
379+
// Number types, only lossless operations
380+
//TODO: use (a*b) when using ion-rs > rc.11
381+
(Int(a), Int(b)) => (a.expect_i128().unwrap() * b.expect_i128().unwrap()).into(),
382+
(Float(a), Float(b)) => (a * b).into(),
383+
(Decimal(a), Decimal(b)) => a.mul(b).into(),
384+
(Decimal(a), Int(b)) | (Int(b), Decimal(a)) => a.mul(b).into(),
385+
386+
// Only try potentially lossy Float conversions when we've run out of the other options
387+
(a @ Int(_) | a @ Decimal(_), Float(b)) | (Float(b), a @ Int(_) | a @ Decimal(_)) => {
388+
let Some(f) = a.clone().to_f64() else {
389+
return jaq_unary_error(a, "cannot be an f64");
390+
};
391+
(f * b).into()
392+
}
393+
394+
(a, b) => return jaq_binary_error(a, b, "cannot be multiplied"),
395+
};
396+
397+
Ok(JaqElement::from(elt))
323398
}
324399
}
325400

326401
impl Div for JaqElement {
327402
type Output = ValR<Self>;
328403

404+
/// From: https://jqlang.org/manual/#multiplication-division-modulo
405+
///
406+
/// > Dividing a string by another splits the first using the second as separators.
329407
fn div(self, _rhs: Self) -> Self::Output {
330-
todo!()
408+
let (lhv, rhv) = (self.into_value(), _rhs.into_value());
409+
410+
use ion_math::{DecimalMath, ToFloat};
411+
use Value::*;
412+
413+
let elt: Element = match (lhv, rhv) {
414+
// Dividing a string by another splits the first using the second as separators.
415+
(String(a), String(b)) => {
416+
let split = a.text().split(b.text());
417+
let iter = split.map(|s| String(s.into())).map(Element::from);
418+
ion_rs::List::from_iter(iter).into()
419+
}
420+
(Symbol(a), Symbol(b)) => match (a.text(), b.text()) {
421+
(Some(ta), Some(tb)) => {
422+
let iter = ta.split(tb).map(|s| Symbol(s.into())).map(Element::from);
423+
ion_rs::List::from_iter(iter)
424+
}
425+
//TODO: Handle symbols with unknown text?
426+
_ => return jaq_binary_error(Symbol(a), Symbol(b), "cannot be divided"),
427+
}
428+
.into(),
429+
430+
// Number types, only lossless operations
431+
(Int(a), Int(b)) => (a.expect_i128().unwrap() / b.expect_i128().unwrap()).into(),
432+
(Float(a), Float(b)) => (a / b).into(),
433+
(Decimal(a), Decimal(b)) => a.div(b).into(),
434+
(Decimal(a), Int(b)) => a.div(b).into(),
435+
(Int(a), Decimal(b)) => a.div(b).into(),
436+
437+
// Only try potentially lossy Float conversions when we've run out of the other options
438+
(a @ Int(_) | a @ Decimal(_), Float(b)) => {
439+
let Some(f) = a.clone().to_f64() else {
440+
return jaq_unary_error(a, "cannot be an f64");
441+
};
442+
(f / b).into()
443+
}
444+
(Float(a), b @ Int(_) | b @ Decimal(_)) => {
445+
let Some(f) = b.clone().to_f64() else {
446+
return jaq_unary_error(b, "cannot be an f64");
447+
};
448+
(a / f).into()
449+
}
450+
451+
(a, b) => return jaq_binary_error(a, b, "cannot be divided"),
452+
};
453+
454+
Ok(JaqElement::from(elt))
331455
}
332456
}
333457

334458
impl Rem for JaqElement {
335459
type Output = ValR<Self>;
336460

337461
fn rem(self, _rhs: Self) -> Self::Output {
338-
todo!()
462+
let (lhv, rhv) = (self.into_value(), _rhs.into_value());
463+
464+
use ion_math::{DecimalMath, ToFloat};
465+
use Value::*;
466+
467+
let elt: Element = match (lhv, rhv) {
468+
// Number types, only lossless operations
469+
(Int(a), Int(b)) => (a.expect_i128().unwrap() % b.expect_i128().unwrap()).into(),
470+
(Float(a), Float(b)) => (a % b).into(),
471+
(Decimal(a), Decimal(b)) => a.rem(b).into(),
472+
(Decimal(a), Int(b)) => a.rem(b).into(),
473+
(Int(a), Decimal(b)) => a.rem(b).into(),
474+
475+
// Only try potentially lossy Float conversions when we've run out of the other options
476+
(a @ Int(_) | a @ Decimal(_), Float(b)) => {
477+
let Some(f) = a.clone().to_f64() else {
478+
return jaq_unary_error(a, "cannot be an f64");
479+
};
480+
(f % b).into()
481+
}
482+
(Float(a), b @ Int(_) | b @ Decimal(_)) => {
483+
let Some(f) = b.clone().to_f64() else {
484+
return jaq_unary_error(b, "cannot be an f64");
485+
};
486+
(a % f).into()
487+
}
488+
489+
(a, b) => return jaq_binary_error(a, b, "cannot be divided (remainder)"),
490+
};
491+
492+
Ok(JaqElement::from(elt))
339493
}
340494
}
341495

342496
impl Neg for JaqElement {
343497
type Output = ValR<Self>;
344498

345499
fn neg(self) -> Self::Output {
346-
todo!()
500+
let val = self.into_value();
501+
502+
use ion_math::DecimalMath;
503+
use Value::*;
504+
505+
let elt: Element = match val {
506+
// Only number types can be negated
507+
Int(a) => (-a).into(),
508+
Float(a) => (-a).into(),
509+
Decimal(a) => (-a.to_big_decimal()).to_decimal().into(),
510+
511+
other => return jaq_unary_error(other, "cannot be negated"),
512+
};
513+
514+
Ok(JaqElement::from(elt))
347515
}
348516
}
349517

@@ -473,11 +641,15 @@ impl jaq_std::ValT for JaqElement {
473641
}
474642

475643
fn as_isize(&self) -> Option<isize> {
476-
todo!()
644+
match self.0.value() {
645+
Value::Int(i) => i.expect_i64().unwrap().to_isize(),
646+
Value::Decimal(d) => d.to_big_decimal().to_isize(),
647+
_ => None,
648+
}
477649
}
478650

479651
fn as_f64(&self) -> Result<f64, jaq_core::Error<Self>> {
480-
use ion_math::FloatMath;
652+
use ion_math::ToFloat;
481653
self.0
482654
.value()
483655
.clone()
@@ -498,21 +670,30 @@ pub(crate) mod ion_math {
498670
use ion_rs::decimal::coefficient::Sign;
499671
use ion_rs::{Decimal, Int, Value};
500672

501-
pub(crate) trait DecimalMath {
673+
/// We can't provide math traits for Decimal directly, so we have a helper trait
674+
pub(crate) trait DecimalMath: Sized {
502675
fn to_big_decimal(self) -> BigDecimal;
503676
fn to_decimal(self) -> Decimal;
504-
fn add(self, v2: impl DecimalMath) -> Decimal
505-
where
506-
Self: Sized,
507-
{
677+
678+
fn add(self, v2: impl DecimalMath) -> Decimal {
508679
(self.to_big_decimal() + v2.to_big_decimal()).to_decimal()
509680
}
510-
fn sub(self, v2: impl DecimalMath) -> Decimal
511-
where
512-
Self: Sized,
513-
{
681+
682+
fn sub(self, v2: impl DecimalMath) -> Decimal {
514683
(self.to_big_decimal() - v2.to_big_decimal()).to_decimal()
515684
}
685+
686+
fn mul(self, v2: impl DecimalMath) -> Decimal {
687+
(self.to_big_decimal() * v2.to_big_decimal()).to_decimal()
688+
}
689+
690+
fn div(self, v2: impl DecimalMath) -> Decimal {
691+
(self.to_big_decimal() / v2.to_big_decimal()).to_decimal()
692+
}
693+
694+
fn rem(self, v2: impl DecimalMath) -> Decimal {
695+
(self.to_big_decimal() % v2.to_big_decimal()).to_decimal()
696+
}
516697
}
517698

518699
impl DecimalMath for Decimal {
@@ -554,17 +735,17 @@ pub(crate) mod ion_math {
554735
}
555736
}
556737

557-
pub(crate) trait FloatMath {
738+
pub(crate) trait ToFloat {
558739
fn to_f64(self) -> Option<f64>;
559740
}
560741

561-
impl FloatMath for f64 {
742+
impl ToFloat for f64 {
562743
fn to_f64(self) -> Option<f64> {
563744
Some(self)
564745
}
565746
}
566747

567-
impl FloatMath for Int {
748+
impl ToFloat for Int {
568749
fn to_f64(self) -> Option<f64> {
569750
self.as_i128().and_then(|data| {
570751
let float = data as f64;
@@ -573,17 +754,18 @@ pub(crate) mod ion_math {
573754
}
574755
}
575756

576-
impl FloatMath for Decimal {
757+
impl ToFloat for Decimal {
577758
fn to_f64(self) -> Option<f64> {
578759
self.to_big_decimal().to_f64()
579760
}
580761
}
581762

582-
impl FloatMath for Value {
763+
impl ToFloat for Value {
583764
fn to_f64(self) -> Option<f64> {
584765
match self {
585766
Value::Int(i) => i.to_f64(),
586767
Value::Decimal(d) => d.to_f64(),
768+
Value::Float(f) => f.to_f64(),
587769
_ => None,
588770
}
589771
}

0 commit comments

Comments
 (0)