Search code examples
phpmeasurement

Heuristic to sort any "kind" of technical measurements in PHP


I different lists with measurements of the same dimension but a bit mixed units like

"1 m, 200 mm, 1 ft"

or maybe also

"1 °C, 273 K" and so on.

Now I want to sort them by absolute order

"200 mm, 1 ft, 1 m" and "273 K, 1 °C"

I am wondering if this a an already solved problem, as I do not want to reinvent the wheel. I am afraid, this might be some kind of "shopping for PHP extensions" questions, but I already found some helpful packages:

https://github.com/PhpUnitsOfMeasure/php-units-of-measure can do all kind of conversation between units of measure.

I already have created code to separate unit and number.

So what I am thinking, to "brute force" the unit to a certain dimension of those:

https://github.com/PhpUnitsOfMeasure/php-units-of-measure/tree/master/source/PhysicalQuantity

Next I could pick the first dimension and convert everything to the first "main" SI unit and sort it.

Right?


Solution

  • That is the solution I came up with, based on the suggestions

    public function testCompareLength()
    {
        $this->assertLessThan(0, $this->objectDe->compareFunction('100 mm', '1 m'));
    }
    
    public function testCompareTemperature()
    {
        $this->assertLessThan(0, $this->objectDe->compareFunction('1 K', '0 °C'));
        $this->assertGreaterThan(0, $this->objectDe->compareFunction('0 °C', '1 K'));
        $this->assertEquals(0, $this->objectDe->compareFunction('-273 °C', '0 K'));
    }
    

    /**
     * @param $numberString
     *
     * @return array
     */
    public function parseNumber($numberString): array
    {
        $values = preg_split('/(?<=[0-9.,])(?=[^0-9,.]+)/i', $numberString);
    
        $float = $values[0];
        $unit = $values[1] ?? '';
    
        $decPos = strpos($float, '.');
        if ($decPos === false) {
            $precision = 0;
        } else {
            $precision = strlen($float) - $decPos - 1;
        }
    
        return ['float' => $float, 'unit' => $unit, 'precision' => $precision];
    }
    
    
    private function heuristicMeasureFactory($measure)
    {
        $prioritizedDimensions = [
            Temperature::class,
            Length::class,
        ];
    
        $unit = trim($measure['unit']);
    
        foreach ($prioritizedDimensions as $class) {
            foreach ($class::getUnitDefinitions() as $definition) {
                if ($definition->getName() == $unit) {
                    return new $class($measure['float'], $unit);
                }
            }
        }
    
        // now process aliases
        foreach ($prioritizedDimensions as $class) {
            foreach ($class::getUnitDefinitions() as $definition) {
                foreach ($definition->aliases as $alias) {
                    if ($alias == $unit) {
                        return new $class($measure['float'], $unit);
                    }
                }
            }
        }
    
        return null; // NaN
    }
    
    /**
     * Sort apples and oranges -- kind of. Not.
     *
     * Compares two strings which represend a measurement of the same physical dimension
     */
    public function compareFunction($a, $b)
    {
        $definitions = Temperature::getUnitDefinitions();
    
        $aParsed = $this->parseNumber($a);
        $aVal = $this->heuristicMeasureFactory($aParsed);
    
        $bParsed = $this->parseNumber($b);
        $bVal = $this->heuristicMeasureFactory($bParsed);
    
        if ($aVal == null || $bVal == null) {
            return strnatcmp($aVal, $bVal); // fallback to string comparision
        }
    
        return bccomp($aVal->subtract($bVal)->toNativeUnit(), 0, 36);
    }