Search code examples
phpjsonfloating-pointscientific-notation

How to disable scientific notation for values like 0.000005 in PHP (json_encode)?


I'm trying to integrate with some Partner API.

They only accept json with float type for amount.

Example:

  • OK
    • {"amount":0.0000005}
  • Error
    • {"amount":"0.0000005"}
    • {"amount":5.0E-7}

If the value greater or equal 1, then it's OK scenario always. But in my case I have values > 0, and < 1.

Code Example:

$arr = ['amount' => 0.0000005];
$str = json_encode($arr);

echo $str;

Output:

{"amount":5.0e-7}

I want the output to look like this:

{"amount":0.0000005}

Is it possible in php? May be some hacks & tricks?


Solution

  • The cleanest I can think of is to traverse through the data, recursively substituting small numbers with a placeholder; then, after JSON encoding, replace the placeholders in the final JSON string with the number formatted how you want it.

    The surprisingly difficult part is formatting the float itself; I found this existing question about how to do that with some working but not very elegant implementations. For brevity, I've left that part as a TODO below.

    class JsonMangler
    {
        private const THRESHOLD = 0.0001;
        private const PLACEHOLDER = '__PLACEHOLDER__';
    
        private array $mangledData = [];
        private array $substitutions = [];
        private int $placeholderIncrement = 0;
    
        public function __construct(array $realData) {
            // Start the recursive function
            $this->mangledData = $this->mangle($realData);
        }
    
        private function mangle(array $realData): array {
            $mangledData = [];
    
            foreach ( $realData as $key => $realValue ) {
                if ( is_float($realValue) && $realValue < self::THRESHOLD) {
                    // Substitute small floats with a placeholder
                    $substituteValue = self::PLACEHOLDER . ($this->placeholderIncrement++);
                    $mangledData[$key] = $substituteValue;
                    // Placeholder will appear in quotes in the JSON, which we want to replace away
                    $this->substitutions["\"$substituteValue\""] = $this->formatFloat($realValue);
                }
                elseif ( is_array($realValue) ) {
                    // Recurse through the data
                    $mangledData[$key] = $this->mangle($realValue);
                }
                else {
                    // Retain everything else
                    $mangledData[$key] = $realValue;
                }
            }
    
            return $mangledData;
        }
    
        /**
         * Format a float into a string without any exponential notation
         */
        private function formatFloat(float $value): string
        {
            // This is surprisingly hard to do; see https://stackoverflow.com/q/22274437/157957
            return 'TODO';
        }
    
        public function getJson(int $jsonEncodeFlags = 0): string
        {
            $mangledJson = json_encode($this->mangledData, $jsonEncodeFlags);
            return str_replace(array_keys($this->substitutions), array_values($this->substitutions), $mangledJson);
        }
    }
    

    Using this implementation for formatFloat, the following test:

    $example = [
        'amount' => 1.5,
        'small_amount' => 0.0001,
        'tiny_amount' => 0.0000005,
        'subobject' => [
            'sub_value' => 42.5,
            'tiny_sub_value' => 0.0000425,
            'array' => [
                1.23,
                0.0000123
            ]
        ]
    ];
    echo (new JsonMangler($example))->getJson(JSON_PRETTY_PRINT);
    

    Results in the following output:

    {
        "amount": 1.5,
        "small_amount": 0.0001,
        "tiny_amount": 0.0000005,
        "subobject": {
            "sub_value": 42.5,
            "tiny_sub_value": 0.0000425,
            "array": [
                1.23,
                0.0000123
            ]
        }
    }