units(), $b->units() ), $a->scale() ); } /** * Subtract b from a. * * @param MoneyAmount $a Left. * @param MoneyAmount $b Right. * @return MoneyAmount */ public static function sub( MoneyAmount $a, MoneyAmount $b ) { self::assert_same_scale( $a, $b ); return new MoneyAmount( self::isub( $a->units(), $b->units() ), $a->scale() ); } /** * Multiply a money amount by a non-negative integer quantity (O(1) via BCMath). * * Negative quantities are rejected — callers must route credit lines separately. * * @param MoneyAmount $amount Unit amount. * @param int|string $quantity Non-negative integer quantity. * @return MoneyAmount */ public static function multiply_by_integer( MoneyAmount $amount, $quantity ) { Bcmath::require_extension(); if ( is_int( $quantity ) ) { $qty_s = (string) $quantity; } elseif ( is_string( $quantity ) && preg_match( '/^-?\d+$/', $quantity ) ) { $qty_s = $quantity; } else { throw new MoneyException( 'MB_MONEY_PRECISION_INVALID', 'quantity must be an integer' ); } if ( bccomp( $qty_s, '0', 0 ) < 0 ) { throw new MoneyException( 'MB_MONEY_PRECISION_INVALID', 'negative quantity is not allowed for money multiply' ); } if ( 0 === bccomp( $qty_s, '0', 0 ) ) { return MoneyAmount::zero( $amount->scale() ); } $units = bcmul( (string) $amount->units(), $qty_s, 0 ); return new MoneyAmount( self::normalize_numeric( $units ), $amount->scale() ); } /** * Compare two amounts. * * @param MoneyAmount $a Left. * @param MoneyAmount $b Right. * @return int -1|0|1 */ public static function compare( MoneyAmount $a, MoneyAmount $b ) { self::assert_same_scale( $a, $b ); return MoneyAmount::cmp( $a->units(), $b->units() ); } /** * Severity comparison without floating margin: * compare gross_profit * 100 * threshold_scale vs revenue * threshold_scaled. * * Returns -1 if margin < threshold, 0 if equal, 1 if greater. * * @param MoneyAmount $gross_profit Gross profit. * @param MoneyAmount $revenue Revenue (must be > 0). * @param int $threshold_scaled Threshold × 10^4 (e.g. 100000 for 10%). * @param int $threshold_scale Usually 10000. * @return array{cmp:int,left:string,right:string} */ public static function compare_margin_to_threshold( MoneyAmount $gross_profit, MoneyAmount $revenue, $threshold_scaled, $threshold_scale = 10000 ) { self::assert_same_scale( $gross_profit, $revenue ); if ( ! $revenue->is_positive() ) { throw new MoneyException( 'MB_MONEY_PRECISION_INVALID', 'revenue must be positive for margin comparison' ); } $left = self::imul( self::imul( $gross_profit->units(), 100 ), $threshold_scale ); $right = self::imul( $revenue->units(), $threshold_scaled ); return array( 'cmp' => MoneyAmount::cmp( $left, $right ), 'left' => (string) $left, 'right' => (string) $right, ); } /** * @param MoneyAmount $a Left. * @param MoneyAmount $b Right. * @return void */ private static function assert_same_scale( MoneyAmount $a, MoneyAmount $b ) { if ( $a->scale() !== $b->scale() ) { throw new MoneyException( 'MB_MONEY_PRECISION_INVALID', 'scale mismatch' ); } } /** * @param int|string $a Left. * @param int|string $b Right. * @return int|string */ private static function iadd( $a, $b ) { Bcmath::require_extension(); return self::normalize_numeric( bcadd( (string) $a, (string) $b, 0 ) ); } /** * @param int|string $a Left. * @param int|string $b Right. * @return int|string */ private static function isub( $a, $b ) { Bcmath::require_extension(); return self::normalize_numeric( bcsub( (string) $a, (string) $b, 0 ) ); } /** * @param int|string $a Left. * @param int|string $b Right. * @return int|string */ private static function imul( $a, $b ) { Bcmath::require_extension(); return self::normalize_numeric( bcmul( (string) $a, (string) $b, 0 ) ); } /** * @param string $n Numeric string. * @return int|string */ private static function normalize_numeric( $n ) { if ( strlen( ltrim( $n, '-' ) ) < 15 ) { return (int) $n; } return $n; } }