DBXE/classes/Components/I18n/Formatter/FloatFormatter.php

281 lines
8.2 KiB
PHP

<?php
/*
* SPDX-FileCopyrightText: 2023 Roland Rusch, easy-smart solution GmbH <roland.rusch@easy-smart.ch>
* SPDX-License-Identifier: AGPL-3.0-only
*/
declare(strict_types=1);
namespace Xentral\Components\I18n\Formatter;
use Xentral\Components\I18n\Formatter\Exception\TypeErrorException;
/**
* Parse and format float numbers. This class is also used as base for IntegerFormatter and
* CurrencyFormatter.
*
* @author Roland Rusch, easy-smart solution GmbH <roland.rusch@easy-smart.ch>
*/
class FloatFormatter extends AbstractFormatter implements FormatterInterface
{
private \NumberFormatter $numberFormatter;
protected int $parseType = \NumberFormatter::TYPE_DOUBLE;
protected int $formatterStyle = \NumberFormatter::DECIMAL;
/**
* Initialize PHP \NumberFormatter.
*
* @return void
*/
protected function init(): void
{
$this->setMinDigits(0);
$this->setMaxDigits(100);
}
public function isStrictValidPhpVal($input): bool
{
return is_numeric($input);
}
/**
* Parse string from user input and store as float in object.
* If parsing fails, an Exception is thrown.
*
* @param string $input
*
* @return self
*/
public function parseUserInput(string $input): self
{
// Sanitize string
$input = $this->sanitizeInputString($input);
if ($input === '') {
// Check if user has entered an empty value and we are in strictness MODE_NULL
if ($this->getStrictness() == FormatterMode::MODE_NULL) {
$this->setParsedValue(null);
return $this;
}
// Check if user has entered an empty string and we are in strictness MODE_EMPTY
if ($this->getStrictness() == FormatterMode::MODE_EMPTY) {
$this->setParsedValue('');
return $this;
}
// User has entered an empty string, but this is not allowed in strictness MODE_STRICT
throw new TypeErrorException(
"Value " . var_export($input, true) . " is not a valid type for " . get_class(
$this
) . " with strictness {$this->getStrictness()->name}"
);
}
// From here on, $input must contain a parseable input
if (($output = $this->parse($input)) === false) {
throw new \RuntimeException("{$this->getNumberFormatter()->getErrorMessage()}. \$input={$input}");
}
$this->setParsedValue($output);
return $this;
}
/**
* Return a string representing the PHP value as a formatted value.
* Throws an Exception if no value was set before or the value is of the wrong type.
*
* @return string
*/
public function formatForUser(): string
{
return ($this->isNullValidPhpValue($this->getPhpVal()) || $this->isEmptyValidPhpValue($this->getPhpVal()))
? ''
: $this->format($this->getPhpVal());
}
/**
* Return a string that can be used in an SQL query to format the value for presentation to a User.
* Should return the same string as if it was formatted by FormatterInterface::formatForUser(), but directly from
* the database.
* This function does not need a native PHP value, but a table column is needed.
*
* @param string $col
*
* @return string
*/
public function formatForUserWithSqlStatement(string $col): string
{
$min_decimals = $this->getNumberFormatter()->getAttribute(\NumberFormatter::MIN_FRACTION_DIGITS);
$max_decimals = $this->getNumberFormatter()->getAttribute(\NumberFormatter::MAX_FRACTION_DIGITS);
return ("FORMAT({$col},LEAST('{$max_decimals}',GREATEST('{$min_decimals}',LENGTH(TRIM(TRAILING '0' FROM SUBSTRING_INDEX(CAST({$col} AS CHAR),'.',-1))))),'{$this->getLocale()}')");
}
/**
* Set the minimum displayed fraction digits for formatted output.
*
* @param int $digits
*
* @return $this
*/
public function setMinDigits(int $digits): self
{
$this->getNumberFormatter()->setAttribute(\NumberFormatter::MIN_FRACTION_DIGITS, $digits);
return $this;
}
/**
* Set the maximum displayed fraction digits for formatted output.
*
* @param int $digits
*
* @return $this
*/
public function setMaxDigits(int $digits): self
{
$this->getNumberFormatter()->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, $digits);
return $this;
}
/**
* Set the minimum displayed integer digits for formatted output.
*
* @param int $digits
*
* @return $this
*/
public function setMinIntDigits(int $digits): self
{
$this->getNumberFormatter()->setAttribute(\NumberFormatter::MIN_INTEGER_DIGITS, $digits);
return $this;
}
/**
* Set the maximum displayed integer digits for formatted output.
*
* @param int $digits
*
* @return $this
*/
public function setMaxIntDigits(int $digits): self
{
$this->getNumberFormatter()->setAttribute(\NumberFormatter::MAX_INTEGER_DIGITS, $digits);
return $this;
}
/**
* Set the minimum displayed significant digits for formatted output.
*
* @param int $digits
*
* @return $this
*/
public function setMinSignificantDigits(int $digits): self
{
$this->getNumberFormatter()->setAttribute(\NumberFormatter::MIN_SIGNIFICANT_DIGITS, $digits);
return $this;
}
/**
* Set the maximum displayed significant digits for formatted output.
*
* @param int $digits
*
* @return $this
*/
public function setMaxSignificantDigits(int $digits): self
{
$this->getNumberFormatter()->setAttribute(\NumberFormatter::MAX_SIGNIFICANT_DIGITS, $digits);
return $this;
}
/**
* Return a \NumberFormatter object from cache. If the object does not exist, it is created first.
*
* @return \NumberFormatter
*/
protected function getNumberFormatter(): \NumberFormatter
{
if (!isset($this->numberFormatter)) {
$this->numberFormatter = new \NumberFormatter($this->getLocale(), $this->formatterStyle);
$this->numberFormatter->setAttribute(\NumberFormatter::LENIENT_PARSE, 1);
}
return $this->numberFormatter;
}
protected function sanitizeInputString(string $string): string
{
return trim(strtolower(stripslashes(strval($string))), "abcdefghijklmnopqrstuvwxyz \t\n\r\0\x0B");
}
/**
* Internal parse function. Calls \NumberFormatter::parse().
*
* @param string $input
* @param \NumberFormatter|null $numberFormatter
*
* @return false|float|int
*/
protected function parse(string $input, \NumberFormatter|null $numberFormatter = null): false|float|int
{
if ($numberFormatter === null) {
$numberFormatter = $this->getNumberFormatter();
}
if (($output = $numberFormatter->parse($input, $this->parseType)) === false) {
// could not parse number
// as a last resort, try to parse the number with locale de_DE
// This is necessary, because of many str_replace, where dot is replaced with comma
$numFmt = new \NumberFormatter('de_DE', \NumberFormatter::DECIMAL);
$output = $numFmt->parse($input, $this->parseType);
}
return $output;
}
/**
* Internal format function. Calls \NumberFormatter::format().
*
* @param float|int $phpVal
* @param \NumberFormatter|null $numberFormatter
*
* @return false|string
*/
protected function format(float|int $phpVal, \NumberFormatter|null $numberFormatter = null): false|string
{
if ($numberFormatter === null) {
$numberFormatter = $this->getNumberFormatter();
}
return $numberFormatter->format($phpVal);
}
}