Amount.php
Pfad: src/Domain/Rechnung/Amount.php
Ext: php
Größe: 3758 Bytes
Geändert: 2026-07-16 16:57:10+02
<?php
declare(strict_types=1);
namespace Demo\Domain\Rechnung;
use InvalidArgumentException;
/**
* Exakter Geldbetrag mit vier Dezimalstellen ohne binaere Float-Arithmetik.
*/
final class Amount
{
public readonly string $netto;
public readonly string $steuer;
public readonly string $brutto;
public readonly string $währung;
/**
* @param string|int|float $netto Nettobetrag.
* @param string|int|float $steuer Steuerbetrag.
* @param string|int|float $brutto Bruttobetrag.
* @param string $währung ISO-Waehrung.
*/
public function __construct(string|int|float $netto, string|int|float $steuer, string|int|float $brutto, string $währung)
{
$this->netto = self::decimal($netto);
$this->steuer = self::decimal($steuer);
$this->brutto = self::decimal($brutto);
$this->währung = strtoupper(trim($währung));
}
/**
* Normalisiert einen lokalen Dezimalwert auf vier Nachkommastellen.
*/
public static function decimal(string|int|float $value): string
{
$raw = is_float($value) ? sprintf('%.4F', $value) : trim((string) $value);
$raw = str_replace(["\u{00A0}", ' '], '', $raw);
if (str_contains($raw, ',') && str_contains($raw, '.')) {
$raw = strrpos($raw, ',') > strrpos($raw, '.')
? str_replace(',', '.', str_replace('.', '', $raw))
: str_replace(',', '', $raw);
} elseif (str_contains($raw, ',')) {
$raw = str_replace(',', '.', $raw);
}
if (preg_match('/^-?\d+(?:\.\d{1,4})?$/', $raw) !== 1) {
throw new InvalidArgumentException('Ungueltiger Dezimalbetrag');
}
[$whole, $fraction] = array_pad(explode('.', $raw, 2), 2, '');
return $whole . '.' . str_pad($fraction, 4, '0');
}
/**
* Prueft Netto plus Steuer gegen Brutto in Integer-Arithmetik.
*/
public function isConsistent(float $toleranz): bool
{
$limit = (int) round($toleranz * 10_000);
return abs(self::units($this->netto) + self::units($this->steuer) - self::units($this->brutto)) <= $limit;
}
/**
* Prueft auf einen positiven Bruttobetrag und gueltige Waehrung.
*/
public function isPositive(): bool
{
return self::units($this->brutto) > 0 && preg_match('/^[A-Z]{3}$/', $this->währung) === 1;
}
/**
* Subtrahiert zwei Dezimalwerte verlustfrei.
*/
public static function subtract(string|int|float $minuend, string|int|float $subtrahend): string
{
return self::fromUnits(self::units(self::decimal($minuend)) - self::units(self::decimal($subtrahend)));
}
/**
* Serialisiert den exakten Betrag.
*
* @return array{netto:string,steuer:string,brutto:string,currency:string}
*/
public function toArray(): array
{
return ['netto' => $this->netto, 'steuer' => $this->steuer, 'brutto' => $this->brutto, 'currency' => $this->währung];
}
/**
* Wandelt vier Dezimalstellen verlustfrei in Integer-Einheiten.
*/
private static function units(string $value): int
{
$negative = str_starts_with($value, '-');
$unsigned = ltrim($value, '-');
[$whole, $fraction] = explode('.', $unsigned, 2);
$units = ((int) $whole * 10_000) + (int) $fraction;
return $negative ? -$units : $units;
}
/**
* Wandelt Integer-Einheiten in einen Dezimalstring.
*/
private static function fromUnits(int $units): string
{
$sign = $units < 0 ? '-' : '';
$absolute = abs($units);
return $sign . intdiv($absolute, 10_000) . '.' . str_pad((string) ($absolute % 10_000), 4, '0', STR_PAD_LEFT);
}
}
Frühere Versionen
| Version | Zeitpunkt | Operation |
|---|---|---|
| 17 | 2026-07-16 19:01:40.811561+02 | UPDATE |