📜 ⬆️ ⬇️

Create a calculator with units

Once I needed to implement a calculator for folding and converting physical quantities. I did not have time limits at that time, so I solved the problem at a high level of abstraction and, accordingly, under a wide range of tasks. I offer you my decision.

image

Imagine that you need to write a calculator that can not just count the numbers, but operate with physical (measured) values ​​- add the length, convert the amount of something from one unit of measure to another, etc. First of all, let's define a slightly more specific task. We will have these features here:


Domain Analysis


In the process of analysis, I will move from the general to the particular.

First of all, it is clear that there is a kind of two dimensions here - physics and mathematics. For physics, the dimension of each of the operators is important. After all, 5 meters / 2 seconds is not 5 kg / 2 m2 for units of measurement are different. And for mathematics, 5 meters * 100 differs from 5 km * 0.1 , since there are different numbers involved.
')
To begin with, we introduce the notion of expression. Let it be something that the calculator will operate on. The simplest expressions can be things from the category of 5 meters , 45 (just a dimensionless constant), etc. But expressions can be more complicated: 10 meters + 2 cm or 5 kg * 45 .

Anyway, any expression should know (or be able to find) 2 of its properties:

  1. absolute value (mathematics)
  2. dimension (physics)

The absolute value can be found by reducing the units of measurement to their SI base unit and calculating the numbers. For an expression of 10 km / 3 hours, the absolute value will be ~ 1.39: 10 km / 3 hours = 10,000 m / 7,200 s = 1.39 m / s . Well, the physical dimension is already obvious: m / s .

And for the expression 5 * 2, the absolute value is 10, the physical dimension is zero (nothing).
At the expense of operators (actions) we can build more complex ones from simple expressions and this complexity has no limits.

Physical dimension


It is intuitively clear for us that 2 km and 2 m2 / 10 cm have the same dimension - meters. And how to explain this to a computer? To begin, we need basic physical measurements. We will not reinvent the wheel and take SI units of measurement as basic:


Then the dimension of any expression can be represented as a vector of dimension. The length of such a dimension vector will be equal to the number of known basic units of measurement. The value opposite each of the basic dimensions will indicate the degree of dimensionality for that dimension.

image

Then from the standpoint of physics, we always look at the dimension (dimension vector) of expressions. For example: 10 km or 1 m - the vector of dimension for both expressions is the same, although they use two different units of measurement.

With mathematics (absolute value), I hope, it is not necessary to explain strongly - just consider the dial as on a regular calculator.

Complex Units


We still need to learn how to decompose complex (non-basic) units of measurement to basic ones — it will be useful both for physics (well, tell me a vector of dimension 1 Joule ) and for mathematics (what is the absolute value of 8 km ).

Imagine that we will have a reference table, where the key is the unit of measurement, and the expression value, which splits this unit of measurement into basic ones, while maintaining the dimension and absolute value of the original complex unit of measurement.

Accordingly, if the units of measurement are not in the reference table, then such a unit is already basic (indecomposable) and therefore it is included in the dimension vector. Let's call such reference table a decomposition table, since she explains how to decompose complex units of measurement into basic ones.

image

Please note that decomposition can occur both at the physical level (Newton is decomposed exclusively into other units of measurement, in absolute value it does not change) and mathematically (kilometer is decomposed into its basic unit of measurement and “corrected” numerically).

Public (high-level) interface


Here we have already felt certain abstractions that are hiding behind the task. Now let's think of the interface that we want to provide outside, and which our calculator undertakes to implement. At this point, it is important to design the interface so that it is elegant, easy to understand and use outside.

Of course, in the center of the scene you need to put our abstraction expression. The input to the calculator will be given some complex expressions with multiplication, sum, etc., and at the output it should give an equivalent expression, which has already been calculated and reduced to the simplest form.

Expression interface


What methods do we need in this interface expression? I suggest the following:


Notice, we can argue that two expressions are equal if and only if their dimension vectors are equal and when their absolute value is the same. Here are some examples:


Dimension interface


We also have the concept of vector dimension. It would be nice to introduce some interface for working with dimensions. It will be appropriate for us to define the following operations on dimension vectors:


Operator Interface


I have already mentioned that we need the concept of a mathematical operation. It's time to look at it in detail. First of all, we have already determined that the “workhorse” of our calculator is an interface expression. Our calculator should certainly be able to fully work with operators, it follows from this that the operator interface must extend the expression interface. By the way, this is very logical! If 10 m is an expression, then why not 10 m + 20 cm not be an expression?

This means that operators should be able to calculate their dimension, calculate their absolute value, and other functionality implied by the MathematicalExpression interface.

But operators are obviously a bit more than just an expression. Let's add the following methods to the operator interface:


Again, it is logical to allow someone outside to explore the left and right operand in case someone wants to somehow analyze an already existing expression. This is a good example of a complete interface - I don’t know why someone might need left and right operands, but it sounds damn like an integral part of the operator interface.

Distribution of responsibilities between the application layer and the database


In the formulation of the problem, we agreed that we would store these expressions in the database, and that the system should be productive on a scale of tens to hundreds of thousands of expressions. This means that the sorting by absolute value must be performed inside the database. Otherwise we will be very slow.

There are literally three points that need to be said:


Internal implementation


In this place we will draw a fat line. We have finished discussing the high-level interface that our calculator provides and are starting to move deeper (jungle) of its implementation.

Dimension vector interface


Here, a rather trivial implementation is just some dictionary, where the key is the basic dimensions, and the value is the degree of the dimension. Vector addition and subtraction are implemented by several lines in any programming language.

The isEqual(Dimension $dimension1, Dimension $dimension2) : bool function isEqual(Dimension $dimension1, Dimension $dimension2) : bool can be implemented as follows:

  1. Subtract one dimension from another.
  2. If the resulting vector consists solely of zeros, then the dimensions are the same.

Expression interface


In the expression interface, we still need to add one “internal” method that will make life easier for us when implementing the formatQuantity() method. A method called containsDimensionlessMember() will return a bool and indicate whether this expression can somehow mutate to be numerically equal to some value. Since only a constant affects the numerical value, so the method is called “haveLiTsF“ dimensionlessChlen ”.

This interface will have 2 implementations: under the unit of measurement and under the constant. So it will be very convenient - the unit of measurement dictates the dimension and, if necessary, it can / should be decomposed, while the constant is obviously dimensionless and only affects the absolute value of the result.

I’ll just give you a listing of PHP code here. I think it will be easier than writing in words.

Constant


Calculate the dimension of the expression
  /** * Determine physical dimension of this mathematical expression. * * @return array * Dimension array of this mathematical expression */ public function dimension() { //      . return array(); } 


You have the dimensionless member ()
  /** * Test whether this mathematical expression includes a dimensionless member. * * Whether this mathematical expression contains at least 1 dimensionless * member. * * @return bool * Whether this mathematical expression contains at least 1 dimensionless * member */ public function containsDimensionlessMember() { return TRUE; } 


Enter the required absolute value in the expression
  /** * Format a certain amount of quantity within this mathematical expression. * * @param float $quantity * Quantity to be formatted * * @return MathematicalExpression * Formatted quantity into this mathematical expression. Sometimes the * mathematical expression itself must mutate in order to format the * quantity. So the returned mathematical expression may not necessarily be * the mathematical expression on which this method was invoked. For * example, the expression "unit" would mutate into "1 * unit" in order to * have a dimensionless member and therefore be able to format the $quantity */ public function formatQuantity($quantity) { //     “   -”, //      . $this->constant = $quantity; return $this; } 


Calculate the absolute value of the expression
  /** * Numerically evaluate this mathematical expression. * * @return float * Numerical value of this mathematical expression */ public function evaluate() { //     . return $this->constant; } 


Expression decomposition
  /** * Decompose (simplify) this mathematical expression. * * @return MathematicalExpression * Decomposed (simplified) version of this mathematical expression */ public function decompose() { //   ,    . return $this; } 


Unit of measurement


To simplify the article, let's leave beyond its scope the issues of working with the decomposition reference table (the table where we store data on how non-basic units of measurement can be decomposed into basic ones). Just imagine that in our class “units” there is already a property, and decomposition is written to it. If this is a basic unit, then this property is not initialized.

Calculate the dimension of the expression
  /** * Determine physical dimension of this mathematical expression. * * @return array * Dimension array of this mathematical expression */ public function dimension() { //    ,      . //       ,      //    . return is_object($this->decomposition) ? $this->decompose()->dimension() : array($this->identifier() => 1); } 


You have the dimensionless member ()
  /** * Test whether this mathematical expression includes a dimensionless member. * * Whether this mathematical expression contains at least 1 dimensionless * member. * * @return bool * Whether this mathematical expression contains at least 1 dimensionless * member */ public function containsDimensionlessMember() { //  ,   ,  ,   //  . return FALSE; } 


Enter the required absolute value in the expression
  /** * Format a certain amount of quantity within this mathematical expression. * * @param float $quantity * Quantity to be formatted * * @return MathematicalExpression * Formatted quantity into this mathematical expression. Sometimes the * mathematical expression itself must mutate in order to format the * quantity. So the returned mathematical expression may not necessarily be * the mathematical expression on which this method was invoked. For * example, the expression "unit" would mutate into "1 * unit" in order to * have a dimensionless member and therefore be able to format the $quantity */ public function formatQuantity($quantity) { //       -,   . //   –  ,  –    . //      ,      . //      ,     $quantity. //        ,    // “ ”. // We expand this unit into "1 * $this" so we get a dimensionless // member that can be formatted. return (new MathematicalExpression(1 * “ . $this->toString()))->formatQuantity($quantity); } 


Calculate the absolute value of the expression
  /** * Numerically evaluate this mathematical expression. * * @return float * Numerical value of this mathematical expression */ public function evaluate() { //       ,  //       . //    ? - ,   “”, //   “ ”. return is_object($this->decomposition) ? $this->decompose()->evaluate() : NULL; } 


Expression decomposition
  /** * Decompose (simplify) this mathematical expression. * * @return MathematicalExpression * Decomposed (simplified) version of this mathematical expression */ public function decompose() { //     ,  . if (is_object($this->decomposition)) { return $this->decomposition->decompose(); } //         . return $this; } 


Mathematical operation


Here implementation will be a little more difficult.

First, let's define the constants and units through the multiplication sign, i.e. 10 meter actually should be written in the form of 10 * meter . This is very useful because allows you to write only units of measurement meter / second and their combination of any complexity: 10 * meter / second or 10 * meter / (2 * second) .

We will take the binary tree as a basis. In the tree node there will be an operator, and two children will have its operands. With this construction we can build expressions of any complexity. The expression 10 * meter + 20 * inch will look like this:

image

Note that in the leaves of such a tree (the terminal node) there will be either units of measurement or constants, and in the other nodes - operators.

For the most part, the operator implementation delegates the calls in the correct order to its two operands. Our 4 operators differ from each other only in read places. Therefore, I wrote a single implementation class and parameterized such a few places through the $this->operator property, the following parameters are packed in this property:


Calculate the dimension of the expression
  /** * Determine physical dimension of this mathematical expression. * * @return array * Dimension array of this mathematical expression */ public function dimension() { //      . //    /,   . //     /,  . , //        . // //       . //          // .  “meter ^ 2”   “meter ^ 3”.   //     dimension callback –    //   ,     . $dimension_callback = $this->operator['dimension callback']; list($evaluate1, $evaluate2) = $this->evaluateOperands(); return $dimension_callback($this->operand1->dimension(), $this->operand2->dimension(), $evaluate1, $evaluate2); } 


You have the dimensionless member ()
  /** * Test whether this mathematical expression includes a dimensionless member. * * Whether this mathematical expression contains at least 1 dimensionless * member. * * @return bool * Whether this mathematical expression contains at least 1 dimensionless * member */ public function containsDimensionlessMember() { //     ,        //   . return $this->operand1->containsDimensionlessMember() || $this->operand2->containsDimensionlessMember(); } 


Enter the required absolute value in the expression
  /** * Format a certain amount of quantity within this mathematical expression. * * @param float $quantity * Quantity to be formatted * * @return MathematicalExpression * Formatted quantity into this mathematical expression. Sometimes the * mathematical expression itself must mutate in order to format the * quantity. So the returned mathematical expression may not necessarily be * the mathematical expression on which this method was invoked. For * example, the expression "unit" would mutate into "1 * unit" in order to * have a dimensionless member and therefore be able to format the $quantity */ public function formatQuantity($quantity) { $contains_dimensionless1 = $this->operand1->containsDimensionlessMember(); $contains_dimensionless2 = $this->operand2->containsDimensionlessMember(); list($quantity1, $quantity2) = $this->evaluateOperands(); //  ,        2 ,  //    –     ,   // .  : (“1 * meter”)->formatQuantity(100);    //     , ..  2  ()  // “” . if ($contains_dimensionless1 xor $contains_dimensionless2) { if ($contains_dimensionless1) { $this->operand1->formatQuantity($quantity / $quantity2); } else { $this->operand2->formatQuantity($quantity / $quantity1); } } else { //     ,     . //   “1 * foot + 1 * inch”.      //     ,      - // “” ,       . //     “”    //  .         // split quantity    . $split_quantity = $this->operator['split quantity callback']; list($quantity1, $quantity2) = $split_quantity($quantity, $quantity1, $quantity2, $this->operator); //    “”     , . $this->operand1->formatQuantity($quantity1); $this->operand2->formatQuantity($quantity2); } return $this; } 


Calculate the absolute value of the expression
  /** * Numerically evaluate this mathematical expression. * * @return float * Numerical value of this mathematical expression * * @throws UnitsMathematicalExpressionDimensionException * Exception is thrown if this mathematical expression has inconsistency in * physical dimensions */ public function evaluate() { //   ,    . if ($this->operator['dimension check'] && !units_dimension_equal($this->operand1->dimension(), $this->operand2->dimension())) { throw new UnitsMathematicalExpressionDimensionException(); } list($evaluate1, $evaluate2) = $this->evaluateOperands(); //       ,   //   –  $evaluate1  $evaluate2  / //   ,  /    ,  //     $this->operator. $evaluate_callback = $this->operator['evaluate callback']; return $evaluate_callback($evaluate1, $evaluate2); } 


Expression decomposition
  /** * Decompose (simplify) this mathematical expression. * * @return MathematicalExpression * Decomposed (simplified) version of this mathematical expression */ public function decompose() { //         //           . return new OperatorMathematicalExpression($this->operator, $this->operand1()->decompose(), $this->operand2()->decompose()); } 


Methods operand1 () and operand2 ()
  /** * Retrieve operand #1 from this mathematical operator. * * @return MathematicalExpression * Operand #1 from this mathematical expression */ public function operand1() { return $this->operand1; } /** * Retrieve operand #2 from this mathematical operator. * * @return MathematicalExpression * Operand #2 from this mathematical expression */ public function operand2() { return $this->operand2; } 


Auxiliary method for calculating the absolute values ​​of the operands
  /** * Numerically evaluate both operands and return them as an array. * * @return array * Array of length 2: the 2 operands numerically evaluated */ protected function evaluateOperands() { $evaluate1 = $this->operand1->evaluate(); $evaluate2 = $this->operand2->evaluate(); //  ,     –   , //         // ,     . //  ,   0.   – . //    =  * 1   6 .   //   ,    (  //   )   //  ,   . if (is_null($evaluate1)) { $evaluate1 = $this->operator['transparent operand1']; } if (is_null($evaluate2)) { $evaluate2 = $this->operator['transparent operand2']; } return array($evaluate1, $evaluate2); } 



Such an implementation perfectly covers the needs of sum, subtraction, multiplication and division. In conclusion, I will give a listing of some callbacks.

Callbacks for the amount


Calculating the absolute value for the sum
 function units_operator_add_evaluate($operand1, $operand2) { //    . return $operand1 + $operand2; } 


Calculation of the resulting dimension for the sum
 function units_operator_add_dimension($dimension1, $dimension2, $operator1, $operator2) { // ..       ,  //         . return $dimension1; } 


The distribution of the absolute number between 2 sum operands
 function units_operator_add_split_quantity($total_quantity, $quantity1, $quantity2, $operator) { //    :  ,     //  (   +    ),   .  //     ,    . ..  //        “ ”    //  . $greatest_quantity = max($quantity1, $quantity2); $for_greater_quantity = floor($total_quantity / $greatest_quantity) * $greatest_quantity; $for_fewer_quantity = $total_quantity - $for_greater_quantity; return $quantity1 > $quantity2 ? array($for_greater_quantity, $for_fewer_quantity) : array($for_fewer_quantity, $for_greater_quantity); } 



Kolbeki for multiplication


Calculation of the absolute value for multiplication
 function units_operator_multiply_evaluate($operand1, $operand2) { return $operand1 * $operand2; } 


Calculation of the resulting dimension for multiplication
 function units_operator_multiply_dimension($dimension1, $dimension2, $operator1, $operator2) { //    . return units_dimension_add($dimension1, $dimension2); } 


The distribution of the absolute number between 2 multiplication operands
 function units_operator_multiply_split_quantity($total_quantity, $quantity1, $quantity2, $operator) { //      formatQuantity()     //  ,      : // 1 * foot + 1 * inch.   1 * foot  1 * inch ,    . //          –   //    ,      // . return array($total_quantity, $operator['transparent operand2']); } 


Similarly, Kolbeks look for other operations.

Tasks with an asterisk *


At school, at the very end of the lesson, the textbook had assignments with an asterisk — the top five with a plus. In this section I have placed such additional tasks, the implementation of which I will not include in the article.

Database


We said that the database should be able to numerically calculate the value of expressions so that they can be sorted and filtered ( show the list of students sorted by height or show students above 3 feet + 5 inches ). I tried different ways to implement this task in the SQL toolkit and eventually came to recursive CTE (recursive Common Table Expressions).

Human-friendly interface


It would be necessary to implement the conversion of our expressions into a string that is convenient for human perception (this notation is called infix). You can add a method to the expression interface toInfix(). But we also need the inverse operation — something that can parse an infix expression into our binary tree. This problem is solved rather trivially in the presence of Wikipedia and sensible brain. Read about parsing infix and postfix notation. The presence of such an infix parser makes life much easier for a programmer, since expressions can be assembled by a simple call units_mathematical_expression_create_from_infix(“10 * meter * kilogram / second”). And not a cumbersome construction in several lines, where for each operand we create our own object and put one object into another to build the final binary tree.

Additional operations


Mathematics is not limited to only four operations. There is also a degree, root, factorial. In my implementation, in addition to the four considered here, there is also a degree.
Moreover, some operators have only 1 operand: factorial, for example. This means that my model can and should be done more flexibly if we want to support more operations.

Nonlinear conversions


And the challenge is straight with two asterisks **! The whole model that I described works only with linear conversions, i.e. with such units of measurement, which are converted among themselves by multiplying by some factor.

Such a conversion scheme is very practical, and therefore very popular: 100 meters in a meter, 12 inches in a foot. But there is one snag, and the name of that snag is Fahrenheit degrees. Honestly, I find it difficult to imagine how anyone could come up with such a temperature scale ... And then someone could take it as the main one to measure the temperature. But alas, the fact of the past days, and we, the descendants of those husbands, have no choice but to adapt to the situation.

The problem with Fahrenheit is as follows. Quote from wikipedia:
On the Fahrenheit scale, the melting temperature of ice is +32 ° F, and the boiling point of water is +212 ° F (at normal atmospheric pressure). In this case, one degree Fahrenheit is equal to 1/180 of the difference between these temperatures.
Perhaps this definition is not so obvious, but Fahrenheit is not converted linearly to Celsius. There is no such factor, multiplying by which, we convert one into another.

tC=59(tF32)


tF=95tC+32


And now the magic, watch your hands carefully: take 100 meters. Imagine them in the form of kilometers: 0.1 km. Multiply both options by 10. We get 1000 meters and 1 km. We give both options to meters and see the expected result - both expressions are equal.

Repeat the trick with the temperature: take 10 C. Imagine in the form of Fahrenheit (50 F). Multiply by 10. Get 100 C and 500 F. Convert both into Celsius: 100 C and 260 C. What is it? By performing the same operation in two different ways, we received very different answers. This means that Celsius and Fahrenheit use fundamentally different scales and no math between them is applicable! At least without any adaptation of this mathematics, well or not all 100% of this mathematics.

Try to analyze what can be done with this problem.

Conclusion


I will allow myself the last paragraph in order to turn back and look at the solution I described from an architectural point of view. Did you find the task difficult at the beginning? Is the proposed architecture easy to understand? Have you noticed that to solve it, I used 3 interfaces, 3 objects and a dozen functions?

Is it convenient to extend such a calculator to additional basic units of measurement (for example, currency: dollars, euro) - easily! Even the code does not need to be edited. Whether it is possible to add new mathematical operations - it is possible, by means of local changes to obviously determined places.

Is it convenient to use such a calculator? Is it good with the portability (reuse) of such a system? - It seems to be good, it can be used at a purely software API level, you can fasten some user interface with buttons. Re-write the implementation to another programming language also does not imply any particular complexity.

The architecture is not overloaded with abstractions; each actor performs one exact self-sufficient operation, which means that it is not easy to “not understand” the meaning of an operation and it is difficult to use it for other purposes. All functions / methods in their implementation do not occupy more than 20 lines of code. Putting it all on unit testing is trite. Architecture on the similarity of this, and I call it elegant. They are beautiful and concise.

Source: https://habr.com/ru/post/359198/


All Articles