minor = $minor; } /** * Creates money from minor units. * * @since 0.1.0 * * @param int $minor Amount in minor units. * @return self */ public static function from_minor( int $minor ): self { return new self( $minor ); } /** * Zero. * * @since 0.1.0 * * @return self */ public static function zero(): self { return new self( 0 ); } /** * Creates money from a decimal amount such as "1250.50". * * Extra decimals are rounded half away from zero. * * @since 0.1.0 * * @param string|int|float $value Decimal amount. * @param int $decimals Currency decimals, 0 to 4. * @return self * * @throws InvalidArgumentException When the value is not a plain decimal number. */ public static function from_decimal( $value, int $decimals = 2 ): self { if ( $decimals < 0 || $decimals > 4 ) { throw new InvalidArgumentException( 'Decimals must be between 0 and 4.' ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped } $string = is_float( $value ) ? sprintf( '%.8F', $value ) : trim( (string) $value ); if ( ! preg_match( '/^(-)?(\d*)(?:\.(\d+))?$/', $string, $match ) || ( '' === $match[2] && empty( $match[3] ) ) ) { throw new InvalidArgumentException( 'Not a valid decimal amount.' ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped } $whole = '' === $match[2] ? '0' : $match[2]; $fraction = $match[3] ?? ''; if ( strlen( $whole ) > 15 ) { throw new InvalidArgumentException( 'Amount is too large.' ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped } $digits = substr( str_pad( $fraction, $decimals, '0' ), 0, $decimals ); $minor = (int) ( $whole . $digits ); if ( strlen( $fraction ) > $decimals && (int) $fraction[ $decimals ] >= 5 ) { ++$minor; } return new self( '-' === $match[1] ? -$minor : $minor ); } /** * Amount in minor units. * * @since 0.1.0 * * @return int */ public function minor(): int { return $this->minor; } /** * Adds another amount. * * @since 0.1.0 * * @param Money $other Other amount. * @return self */ public function add( Money $other ): self { return new self( $this->minor + $other->minor ); } /** * Subtracts another amount. * * @since 0.1.0 * * @param Money $other Other amount. * @return self */ public function subtract( Money $other ): self { return new self( $this->minor - $other->minor ); } /** * Flips the sign. * * @since 0.1.0 * * @return self */ public function negate(): self { return new self( -$this->minor ); } /** * Multiplies by a whole quantity or a factor, rounding half away from zero. * * @since 0.1.0 * * @param int|float $factor Factor. * @return self */ public function multiply( $factor ): self { return new self( (int) round( $this->minor * $factor ) ); } /** * Takes a percentage given in basis points (1500 is 15%), rounding half away from zero. * * @since 0.1.0 * * @param int $basis_points Basis points. * @return self */ public function percent( int $basis_points ): self { $amount = intdiv( abs( $this->minor ) * abs( $basis_points ) + 5000, 10000 ); $sign = ( $this->minor < 0 ) !== ( $basis_points < 0 ) ? -1 : 1; return new self( $sign * $amount ); } /** * Compares with another amount. * * @since 0.1.0 * * @param Money $other Other amount. * @return int -1, 0 or 1. */ public function compare( Money $other ): int { return $this->minor <=> $other->minor; } /** * Tells whether two amounts are equal. * * @since 0.1.0 * * @param Money $other Other amount. * @return bool */ public function equals( Money $other ): bool { return $this->minor === $other->minor; } /** * Tells whether the amount is zero. * * @since 0.1.0 * * @return bool */ public function is_zero(): bool { return 0 === $this->minor; } /** * Tells whether the amount is below zero. * * @since 0.1.0 * * @return bool */ public function is_negative(): bool { return $this->minor < 0; } /** * Tells whether the amount is above zero. * * @since 0.1.0 * * @return bool */ public function is_positive(): bool { return $this->minor > 0; } /** * Plain decimal string such as "1250.50", without symbol or separators. * * @since 0.1.0 * * @param int $decimals Currency decimals, 0 to 4. * @return string */ public function to_decimal_string( int $decimals = 2 ): string { $digits = str_pad( (string) abs( $this->minor ), $decimals + 1, '0', STR_PAD_LEFT ); $whole = 0 === $decimals ? $digits : substr( $digits, 0, -$decimals ); $prefix = $this->minor < 0 ? '-' : ''; return 0 === $decimals ? $prefix . $whole : $prefix . $whole . '.' . substr( $digits, -$decimals ); } /** * Formats the amount for display. * * Options: `symbol`, `position` (before or after), `decimals`, * `thousand_separator`, `decimal_separator`. * * @since 0.1.0 * * @param array $options Format options. * @return string */ public function format( array $options = array() ): string { $options = array_merge( array( 'symbol' => '', 'position' => 'before', 'decimals' => 2, 'thousand_separator' => ',', 'decimal_separator' => '.', ), $options ); $decimals = (int) $options['decimals']; $parts = explode( '.', ltrim( $this->to_decimal_string( $decimals ), '-' ) ); $whole = preg_replace( '/\B(?=(\d{3})+(?!\d))/', (string) $options['thousand_separator'], $parts[0] ); $number = $whole . ( isset( $parts[1] ) ? $options['decimal_separator'] . $parts[1] : '' ); $text = 'after' === $options['position'] ? $number . $options['symbol'] : $options['symbol'] . $number; return ( $this->minor < 0 ? '-' : '' ) . $text; } }