Как-то раз мне нужно было реализовать калькулятор для складывания и конвертации физических величин. У меня тогда не было ограничений по времени, поэтому я решил проблему на высоком уровне абстракции и, соответственно, под широкий спектр задач. Предлагаю на ваш суд мое решение.
dimension() : Dimension
получить вектор размерности нашего выражения. Мы его будем внутренне использовать для валидации физического смысла (мухи можно складывать только с мухами). Будет полезно дать его “наружу” как часть публичного интерфейса, т.к. весьма логично, что кто-то рано или поздно захочет узнать размерность какого-нибудь выражения.evaluate() : float
Расчитать абсолютное значение выражения, т.е. посчитать “математику”.decompose() : MathematicalExpression
разложить все комплексные единицы измерения в выражении на их декомпозицию. Нам этот метод будет полезен как для вычисления абсолютного значения, так и для расчета вектора размерности. Но опять же, звучит весьма логично включить эту операцию в наш публичный интерфейс ведь кому-нибудь такая операция может понадобиться за пределами внутренней реализации нашего калькулятора.formatQuantity(float $quantity)
вписать в наше выражение константы (и подобрать эти константы) так, чтобы численно выражение начало равняться $quantity
. Такой метод будет чуть гибче, чем просто конвертация из одной единицы в другую. Вот пример: американцы используют футы и дюймы для человеческого роста. У меня рост 180 см. Я мог бы сконвертировать это в футы (5.905512) либо в дюймы (70.8661457). Но для американского мозга каноничный вид – это выделить полные футы и остаток записать в виде дюймов – 5 дюймов 10.8 дюймов. Тогда снаружи можно представить себе следующий псевдо-код для форматирования/конвертации (может быть на 2-3 строчки больше кода, но гибкость конструкции на порядок выше):$quantity = $source_expression->evaluate();
$european_height = new MathematicalExpression(“1 * meter”);
$european_height->formatQuantity($quantity);
print $european_height->toString() . “\n”; // 1.8 * meter
$us_height = new MathemticalExpression(“1 * foot + 1 * inch”);
$us_height->formatQuantity($quantity);
print $us_height->toString() . “\n”; // 5 * foot + 10.8 * inch
addDimension(Dimension $dimension1, Dimension $dimension2) : Dimension
Сложить одну размерность с другой. Банально выполняем векторное сложение/вычитание. Это будет полезно при операциях умножения, когда размерности складываются.subtractDimension(Dimension $dimension1, Dimension $dimension2) : Dimension
Вычесть одну размерность из другой. Аналогично с предыдущей операцией, только инвертируется знак. Будет полезно, когда обрабатывается операция деления.isEqual(Dimension $dimension1, Dimension $dimension2) : bool
Сравнить одну размерность с другой. На этом методе будет основываться вся логика валидации физических размерностей. С помощью этой функции мы поймем, имеет ли физический смысл складывать/вычитать два выражения.MathematicalExpression
.operand1() : MathematicalExpression
получить первый (левый) операндoperand2() : MathematicalExpression
получить второй (правый) операндisEqual(Dimension $dimension1, Dimension $dimension2) : bool
можно реализовать следующим образом:formatQuantity()
. Метод под названием containsDimensionlessMember()
будет возвращать bool и указывать на то, может ли это выражение каким-то образом мутироваться, чтобы численно равняться какой-то величине. Т.к. на численное значение влияет лишь константа, поэтому метод так и называется “имеешьЛиТыБезразмерныйЧлен”. /**
* Determine physical dimension of this mathematical expression.
*
* @return array
* Dimension array of this mathematical expression
*/
public function dimension() {
// Мы безразмерный член в любом выражении.
return array();
}
/**
* 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;
}
/**
* 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;
}
/**
* Numerically evaluate this mathematical expression.
*
* @return float
* Numerical value of this mathematical expression
*/
public function evaluate() {
// Численно мы равны самому себе.
return $this->constant;
}
/**
* Decompose (simplify) this mathematical expression.
*
* @return MathematicalExpression
* Decomposed (simplified) version of this mathematical expression
*/
public function decompose() {
// Дальше разлагаться некуда, поэтому возвращаем самого себя.
return $this;
}
/**
* 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);
}
/**
* 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;
}
/**
* 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);
}
/**
* 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;
}
/**
* 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;
}
$this->operator
, в этом свойстве упакованы следующие параметры:$this->operator['evaluate callback']
.$this->operator['dimension check']
.$this->operator['dimension callback']
.formatQuantity()
). Она абстрагирована в $this->operator['split quantity callback']
, который должен распределить абсолютное значение между первым и вторым операндом так, чтобы хотя бы в одном из них была “красивая” константа. /**
* 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);
}
/**
* 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();
}
/**
* 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;
}
/**
* 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);
}
/**
* 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());
}
/**
* 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;
}
/**
* 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);
}
function units_operator_add_evaluate($operand1, $operand2) {
// Банально суммируем два количества.
return $operand1 + $operand2;
}
function units_operator_add_dimension($dimension1, $dimension2, $operator1, $operator2) {
// Т.к. для суммы размерность обоих операндов должна совпадать, то
// достаточно просто вернуть одну из двух размерностей как результирующую.
return $dimension1;
}
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);
}
function units_operator_multiply_evaluate($operand1, $operand2) {
return $operand1 * $operand2;
}
function units_operator_multiply_dimension($dimension1, $dimension2, $operator1, $operator2) {
// При умножении размерности складываются.
return units_dimension_add($dimension1, $dimension2);
}
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']);
}
toInfix()
. Но нужна еще и обратная операция – нечто, что сможет распарсить инфиксное выражение в наше бинарное дерево. Эта задача решается довольно тривиально при наличии Википедии и здравомыслящего мозга. Почитайте о парсинге инфиксной и постфиксной нотации. Наличие такого инфиксного парсера значительно облегчает жизнь программисту, т.к. выражения можно собирать простым вызовом units_mathematical_expression_create_from_infix(“10 * meter * kilogram / second”)
. А не громоздкой конструкцией в несколько строк, где под каждый операнд мы создаем собственный объект и вкладываем один объект в другой для построение конечного бинарного дерева.На шкале Фаренгейта температура таяния льда равна +32 °F, а температура кипения воды — +212 °F (при нормальном атмосферном давлении). При этом один градус Фаренгейта равен 1/180 разности этих температур.Возможно, из этого определения это не так очевидно, но Фаренгейт не конвертируется линейно в Цельсий. Нет такого коэффициента, умножив на который, мы сконвертируем одно в другое.
К сожалению, не доступен сервер mySQL