54 lines
2.0 KiB
PHP
54 lines
2.0 KiB
PHP
<?php
|
|
|
|
namespace App\Support;
|
|
|
|
/**
|
|
* Between what an operator types and what the catalogue stores.
|
|
*
|
|
* Prices are stored in cents — integers, because money in floats eventually
|
|
* bills someone 178.99999999. But the console asked the owner to type cents
|
|
* too, so a €799 plan was entered as 79900, and a slipped digit is a factor of
|
|
* ten on an invoice. The form takes euros; the conversion happens here, once.
|
|
*
|
|
* Parsing is done on the STRING, not by multiplying a float: (float) '79.90'
|
|
* times 100 is 7989.999… on a binary float, and casting that to int truncates
|
|
* to 7989. One cent, every time, on exactly the prices people actually charge.
|
|
*/
|
|
final class Money
|
|
{
|
|
/** What plan_prices.amount_cents can actually hold (unsigned int). */
|
|
public const MAX_CENTS = 4294967295;
|
|
|
|
/**
|
|
* Euros as typed → cents, or null when it is not a price at all.
|
|
*
|
|
* Accepts both separators: a German keyboard produces "79,90" and a copied
|
|
* figure produces "79.90", and rejecting either would just be rude.
|
|
* Thousands separators are refused rather than guessed — "1.000" is a
|
|
* thousand to one reader and one euro to another.
|
|
*/
|
|
public static function toCents(string $euros): ?int
|
|
{
|
|
$value = str_replace(',', '.', trim($euros));
|
|
|
|
if (! preg_match('/^\d{1,9}(\.\d{1,2})?$/', $value)) {
|
|
return null;
|
|
}
|
|
|
|
[$whole, $fraction] = array_pad(explode('.', $value, 2), 2, '');
|
|
|
|
$cents = (int) $whole * 100 + (int) str_pad($fraction, 2, '0');
|
|
|
|
// The column is an unsigned int. Nine whole digits parse happily and
|
|
// then overflow it, turning a typo into a database error on submit
|
|
// instead of a message next to the field.
|
|
return $cents > self::MAX_CENTS ? null : $cents;
|
|
}
|
|
|
|
/** Cents → the string the form shows, with a decimal comma. */
|
|
public static function fromCents(int $cents): string
|
|
{
|
|
return number_format($cents / 100, 2, ',', '');
|
|
}
|
|
}
|