11
$\begingroup$

I have an accrual calculation that I would like to preserve the units even though they are of the same dimension (e.g. time). For example, I would like the following

Quantity[5, "Days"] / Quantity[52, "Weeks"]

to return in units of days per week instead of a unitless number. Later in the process I would like to multiply the result by some number of weeks (Quantity[_, "Week"]) and have the result in day units.

I know about IndependentUnit but prefer that the units in the result still be known to Mathematica (KnownUnitQ true) for other purposes.

I have tried this with UpValues (my first UpValues attempt) with the following but I get an error.

Quantity /: 
 op : (Alternatives @@ (Symbol /@ 
       WolframLanguageData[
        EntityClass["WolframLanguageSymbol", 
          {"FunctionalityArea", "MathOperationFunctions"}], "Name"]))[
   Quantity[v1_, u1_],
   Quantity[v2_, u2_]
   ] :=
 Quantity[op[v1, v2], op[u1, u2]]

Is it possible to do this or force a unit hold in quantity calculations?

I am expecting

Quantity[5, "Days"] / Quantity[52, "Weeks"]

to give the result that the following gives

Quantity[5/52, "Days"/"Weeks"]

(* Quantity[5/52, ("Days")/("Weeks")] *)

enter image description here

$\endgroup$
2
  • $\begingroup$ IndependentUnit are actually KnownUnitQ. Wouldn't that be enough for your purpose? $\endgroup$
    – user31159
    Mar 17, 2016 at 0:01
  • $\begingroup$ @Xavier I should have put CompatibleUnitQ not KnownUnitQ. Else I will not be able to use it with other conversions later on. $\endgroup$
    – Edmund
    Mar 17, 2016 at 0:17

4 Answers 4

3
$\begingroup$

Products of quantities are handled by (see PrintDefinitions[Quantity])

Quantity /: 
HoldPattern[Times][Longest[e2___], q_?QuantityQ, Shortest[e1, ___] /; 
    QuantityUnits`$AutomaticUnitTimes === True := ...

which calls QuantityUnits`Private`qTimes and in turn QuantityUnits`Private`iqTimes. A way to go is to redefine this last function only

(* Ensures the symbol exists from a fresh kernel for unprotecting it *)
System`Quantity;

Unprotect[Quantity];

QuantityUnits`Private`iqTimes[QuantityUnits`Private`q_List] := Module[{units},

        With[{vlist = QuantityUnits`Private`q[[All, 1]], 
              ulist = QuantityUnits`Private`q[[All, 2]]},

             units = Apply[Times, ulist];
             Quantity[Apply[Times, vlist], units]
        ]
];

Protect[Quantity];

This yields the desired outputs

Quantity[5, "Days"] / Quantity[52, "Weeks"]

(* Quantity[5/52, ("Days")/("Weeks")] *)

enter image description here

Quantity[2, "Weeks"] * %

(* Quantity[5/26, "Days"] *)

enter image description here

Your code and the above redefinition could also be enclosed in a Block. Note that even though the modifications seem minimum, there is the possibility that this redefinition breaks other things as it can happen when redefining built-in symbols.

$\endgroup$
6
  • $\begingroup$ This is a scary solution but it works (+1). I am not too keen on Unprotect as there are many warnings about unintended consequences. You also provide those warnings. $\endgroup$
    – Edmund
    Mar 17, 2016 at 13:55
  • $\begingroup$ Since posting the question I developed an almost working solution that is less scary that I will post as an answer. I would appreciate if you would have a look to see if you can fix my outstanding issue with my almost solution. $\endgroup$
    – Edmund
    Mar 17, 2016 at 14:50
  • $\begingroup$ Per scary solution - I agree. This approach was in the line of your proposition with UpValues, which would have required unprotecting Quantity to make it work. Here, it is slightly less dangerous ;). $\endgroup$
    – user31159
    Mar 17, 2016 at 14:54
  • $\begingroup$ Per your post - Ok, I'll have a look at it. $\endgroup$
    – user31159
    Mar 17, 2016 at 14:54
  • $\begingroup$ Actually you don't need the Unprotect at all as you are only making definitions for iqTimes and that is not protected. Of course that doesn't change the fact that you are changing system behavior and that is dangerous, no matter whether the symbol you are manipulating happens to be public and protected or private and not protected ... $\endgroup$ Mar 17, 2016 at 15:03
2
$\begingroup$

Since it's a financial calculation I would think you would want something with a minimum chance of behaving unexpectedly. Hence I offer this very simple approach, which should be completely canonical. It gives an output form close to what you requested:

a = HoldForm[Quantity[5, "Days"]/Quantity[52, "Weeks"]]

$\dfrac{5 \text{ days}}{52\text{ wk}}$

UnitConvert[ReleaseHold@a*Quantity[26., "Weeks"], "Days"]

2.5 days

Or you may prefer this syntax, which works as well:

UnitConvert[a*Quantity[26., "Weeks"], "Days"]//ReleaseHold

And if you don't want to specify HoldForm each time:

f[days_, weeks_] := HoldForm[Quantity[days, "Days"]/Quantity[weeks, "Weeks"]] 
f[5, 52]

$\dfrac{5 \text{ days}}{52\text{ wk}}$

$\endgroup$
1
$\begingroup$

I'm not certain if I should post this as an answer or an edit to the OP. It seems it is more of an answer so I put it here.

Since posting this question have and developed and answer that amost works. It is pattern matching based and works for the "MathOperationFunctions" except for Divide.

qtyUnitHoldRule=
   (op:Alternatives @@ (Symbol /@ 
       WolframLanguageData[
          EntityClass["WolframLanguageSymbol", 
              {"FunctionalityArea", "MathOperationFunctions"}], "Name"])
     )[q1_?QuantityQ, q2_?QuantityQ ] -> 
          Quantity[op @@ QuantityMagnitude[{q1, q2}], op @@ QuantityUnit[{q1, q2}]]

qtyUnitHoldRule will be used to convert move the Quantity arithmetic between two quantities inside of one Quantity object.

Remove[holdQuantityUnits];
Attributes[holdQuantityUnits]={HoldAllComplete};
holdQuantityUnits[code_] := Unevaluated[code] //. qtyUnitHoldRule 

Quantity arithmetic will be wrapped in holdQuantityUnits so that qtyUnitHoldRule can be ReplaceRepeated to convert all instances of this arithmetic.

This works for the "MathOperationFunctions" functions I have tested but does not work for the notation form of Divide (i.e. /).

holdQuantityUnits[
    x=Quantity[5,"Days"];
    y=Quantity[52,"Weeks"];
    {Divide[x,y], x / y}
]

(* {Quantity[5/52, ("Days")/("Weeks")], 5 / 364} *)

Any ideas how I can extend this to include the notation form of Divide?

$\endgroup$
3
  • $\begingroup$ From a fresh kernel on both 10.3 and 10.4, the first evaluation of holdQuantityUnits gives me {5/364, 5/364}, and the second evaluation returns {Quantity[5/52, ("Days")/("Weeks")], Quantity[5/52, ("Days")/("Weeks")]}. I do not have the difference you mention. $\endgroup$
    – user31159
    Mar 17, 2016 at 15:04
  • $\begingroup$ @Xavier Maybe I need a fresh kernel. However, my installed Mathematica has crashed and I am having licensing issues so only have Mathematica Online and it does not appear to like the definition of qtyUnitHoldRule so I may have to wait until I get home to continue. $\endgroup$
    – Edmund
    Mar 17, 2016 at 15:09
  • $\begingroup$ Perhaps this additional rule could make it work: Times[Power[q1_?QuantityQ, p1_.], Power[q2_?QuantityQ, p2_?Negative]] :> Quantity[Divide @@ QuantityMagnitude[{q1, q2}], Divide @@ QuantityUnit[{q1^p1, q2^(-p2)}]]. $\endgroup$
    – user31159
    Mar 17, 2016 at 15:24
0
$\begingroup$

Here's another simple, semi-aggressive way to do this. First we'll note that we have these:

Names["QuantityUnits`$Automatic*"]

{"QuantityUnits`$AutomaticUnitParsing", \
"QuantityUnits`$AutomaticUnitPlus", \
"QuantityUnits`$AutomaticUnitTimes"} 

And so we'll just Block all of them.

withHeldUnits[expr_] :=
  Block[{QuantityUnits`$AutomaticUnitParsing,
    QuantityUnits`$AutomaticUnitPlus,
    QuantityUnits`$AutomaticUnitTimes},
   expr
   ];
withHeldUnits~SetAttributes~HoldFirst

(Quantity["SpeedOfLight"] + Quantity[2, "Angstroms"/"Seconds"])
withHeldUnits@(Quantity["SpeedOfLight"] + 
   Quantity[2, "Angstroms"/"Seconds"])

Quantity[2997924580000000002, ("Angstroms")/("Seconds")]

Quantity[2, ("Angstroms")/("Seconds")] + Quantity[1, "SpeedOfLight"]
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.