Skip to content

Commit

Permalink
Always allow division of identical units (r-quantities#335)
Browse files Browse the repository at this point in the history
* Allow division whenever input units are identical

* Ensure that changes work when one of the inputs is not a units object

* Fix code review issues

* Always allow inverse unit multiplication

* Simplify inverse-unit multiplication and same-unit division

* only call inherits() once per function call
  • Loading branch information
billdenney authored Dec 18, 2022
1 parent 3884458 commit 6c2b593
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 8 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ configure.scan
*.app
# OSX-specific
.DS_Store
inst/doc
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

* Names are preserved when doing unit conversions; #305 @billdenney

* Identical units will always divide (`/`) and allow integer division (`%/%`).
And, inverse units will always be able to multiply; #310 @billdenney

# version 0.8-1

* fix `%/%` and `%%` if arguments have different units; #313
Expand Down
37 changes: 29 additions & 8 deletions R/arith.R
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,41 @@ Ops.units <- function(e1, e2) {
return(NextMethod())

eq <- .Generic %in% c("+", "-", "==", "!=", "<", ">", "<=", ">=") # requiring identical units
div <- .Generic %in% c("/", "%/%") # division-type
mul <- .Generic %in% "*" # multiplication-only
prd <- .Generic %in% c("*", "/", "%/%", "%%") # product-type
pw <- .Generic %in% c( "**", "^") # power-type
pw <- .Generic %in% c("**", "^") # power-type
mod <- .Generic %in% c("%/%", "%%") # modulo-type
pm <- .Generic %in% c("+", "-") # addition-type

if (! any(eq, prd, pw, mod))
stop(paste("operation", .Generic, "not allowed"))

e1_inherits_units <- inherits(e1, "units")
e2_inherits_units <- inherits(e2, "units")
both_inherit_units <- e1_inherits_units && e2_inherits_units
if (eq) {
if (!(inherits(e1, "units") && inherits(e2, "units")))
if (!(both_inherit_units))
stop("both operands of the expression should be \"units\" objects") # nocov
units(e2) <- units(e1) # convert before we can compare; errors if unconvertible
}

if (mod) {
identical_units <-
both_inherit_units &&
identical(units(e1), units(e2))

inverse_units <-
both_inherit_units &&
identical(units(e1)$numerator, units(e2)$denominator) &&
identical(units(e1)$denominator, units(e2)$numerator)

if ((div && identical_units) || (mul && inverse_units)) {
# Special cases for identical unit division and inverse unit multiplication
# which may not be otherwise divisible by udunits (see #310)
e1 <- drop_units(e1)
e2 <- drop_units(e2)
return(set_units(NextMethod(), 1))
} else if (mod) {
div <- e1 / e2
int <- round(div)
if (.Generic == "%/%") {
Expand All @@ -69,10 +89,10 @@ Ops.units <- function(e1, e2) {
return(e1 - int*e2)
}
} else if (prd) {
if (!inherits(e1, "units"))
if (!e1_inherits_units)
e1 <- set_units(e1, 1) # TODO: or warn?

if (!inherits(e2, "units"))
if (!e2_inherits_units)
e2 <- set_units(e2, 1) # TODO: or warn?

ve1 <- unclass(e1) ; ue1 <- units(e1)
Expand All @@ -88,11 +108,12 @@ Ops.units <- function(e1, e2) {
return(.simplify_units(NextMethod(), .symbolic_units(numerator, denominator)))

} else if (pw) {
if (inherits(e2, "units")) {
if (inherits(e1, "units") && identical(units(e1), units(as_units(1))))
if (e2_inherits_units) {
if (e1_inherits_units && identical(units(e1), units(as_units(1)))) {
e1 <- drop_units(e1)
if (inherits(e1, "units"))
} else if (e1_inherits_units) {
stop("power operation only allowed with numeric power")
}

# code to manage things like exp(log(...)) and 10^log10(...) follows
# this is not supported in udunits2, so we are on our own
Expand Down
23 changes: 23 additions & 0 deletions tests/testthat/test_arith.R
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,26 @@ test_that("we obtain mixed units when taking powers of multiple integers", {
p = 4:1
expect_equal(a ^ p, c(set_units(1, m^4), set_units(8, m^3), set_units(9, m^2), set_units(4, m), allow_mixed=TRUE))
})

test_that("identical units can always be divided and return unitless (#310)", {
x1 <- log(set_units(100, "g"))
x2 <- log(set_units(4, "g"))
expect_equal(
x1/x2,
set_units(log(100)/log(4), "")
)
expect_equal(
x1 %/% x2,
set_units(log(100) %/% log(4), "")
)
})

test_that("inverse units can always be multiplied and return unitless (related to #310)", {
x1 <- log(set_units(100, "g"))
x2 <- log(set_units(4, "g"))
expect_equal(
x1*(1/x2),
set_units(log(100)/log(4), "")
)
})

0 comments on commit 6c2b593

Please sign in to comment.