Invision Power Services, Inc. * @copyright (c) Invision Power Services, Inc. * @license https://www.invisioncommunity.com/legal/standards/ * @package Invision Community * @subpackage Nexus * @since 11 Feb 2014 */ namespace IPS\nexus; /* To prevent PHP errors (extending class does not exist) revealing path */ use IPS\Lang; use IPS\Math\Number; use IPS\Member; use IPS\Settings; use NumberFormatter; use function defined; use function floatval; use function function_exists; use function intval; use function is_string; use function strlen; if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) ) { header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' ); exit; } /** * Money Object */ class Money { /** * Get available currencies * * @return array */ public static function currencies() : array { if ( $currencies = json_decode( Settings::i()->nexus_currency, TRUE ) ) { return array_keys( $currencies ); } else { return array( Settings::i()->nexus_currency ); } } /** * Get the number of decimal points used for a currency * * @param string $currency Currency code * @return int */ public static function numberOfDecimalsForCurrency( string $currency ) : int { switch ( $currency ) { case 'CLF': return 4; case 'BHD': case 'IQD': case 'JOD': case 'KWD': case 'LYD': case 'OMR': case 'TND': return 3; case 'MGA': case 'MRO': return 1; case 'BIF': case 'BYR': case 'CLP': case 'CVE': case 'DJF': case 'GNF': case 'ISK': case 'JPY': case 'KMF': case 'KRW': case 'PYG': case 'RWF': case 'UGX': case 'UYI': case 'VND': case 'VUV': case 'XAF': case 'XOF': case 'XPF': return 0; default: return 2; } } /** * @brief Amount */ public ?Number $amount = null; /** * @brief Currency */ public string $currency = ""; /** * @brief Number of decimal points */ protected int $numberOfDecimalPlaces = 2; /** * Contructor * * @param mixed $amount Amount * @param string $currency Currency code * @return void */ public function __construct( mixed $amount, string $currency ) { $this->currency = $currency; $this->numberOfDecimalPlaces = static::numberOfDecimalsForCurrency( $currency ); if ( !( $amount instanceof Number ) ) { if ( is_string( $amount ) ) { $amount = floatval( $amount ); } $amount = new Number( number_format( $amount, $this->numberOfDecimalPlaces, '.', '' ) ); } $this->amount = $amount->round( $this->numberOfDecimalPlaces ); } /** * Amount as string (not formatted, not locale aware) * Used for gateways where, for example, (float) 10.5 is not acceptable, and (string) "10.50" is required * * @return string */ public function amountAsString() : string { return sprintf( "%01." . $this->numberOfDecimalPlaces . "F", (string) $this->amount ); } /** * To string * * @return string */ public function __toString() : string { return $this->toString( Member::loggedIn()->language() ); } /** * To string for language * * @param Lang $language The language to use * @return string */ public function toString( Lang $language ) : string { /* If intl is installed, use NumberFormatter */ if( class_exists( "NumberFormatter" ) and trim( $language->locale['int_curr_symbol'] ) === $this->currency ) { $formatter = new NumberFormatter( $language->short, NumberFormatter::CURRENCY ); if ( floatval( (string) $this->amount ) == intval( (string) $this->amount ) ) { $formatter->setAttribute( NumberFormatter::FRACTION_DIGITS, 0 ); } $result = $formatter->formatCurrency( (string) $this->amount, $this->currency ); /* @see https://bugs.php.net/bug.php?id=54538 */ if( $result !== 'NaN' ) { return $result; } } /* If this currency matches the locale the user is using, and we are not using PHP 7.4, AND money_format is supported (Windows doesn't support it), use that */ if ( function_exists( 'money_format' ) and version_compare( PHP_VERSION, '7.4.0', '<' ) and trim( $language->locale['int_curr_symbol'] ) === $this->currency ) { /* We need to make sure the locale is correct */ $currentLocale = setlocale( LC_ALL, '0' ); $language->setLocale(); /* Get the value to use */ $return = money_format( '%n', (string) $this->amount ); /* Then put the locale back */ Lang::restoreLocale( $currentLocale ); /* And return */ return $return; } /* If it matches any of the installed languages, we can do something with the locale data */ foreach ( Lang::languages() as $lang ) { if ( isset( $lang->locale['int_curr_symbol'] ) AND trim( $lang->locale['int_curr_symbol'] ) === $this->currency ) { $currencySymbol = $lang->convertString( $lang->locale['currency_symbol'] ); $currencySymbolPreceeds = $this->amount->isPositive() ? $lang->locale['p_cs_precedes'] : $lang->locale['n_cs_precedes']; $spaceBetweenCurrencySymbolAndAmount = ( $this->amount->isPositive() ? $lang->locale['p_sep_by_space'] : $lang->locale['n_sep_by_space'] ) ? ' ' : ''; $amount = number_format( (string) $this->amount, $lang->locale['frac_digits'], $lang->locale['mon_decimal_point'], $lang->locale['mon_thousands_sep'] ); $positiveNegativeSymbol = $this->amount->isPositive() ? $lang->locale['positive_sign'] : $lang->locale['negative_sign']; if ( $positiveNegativeSymbol ) { $positiveNegativeSymbolFormat = $this->amount->isPositive() ? $lang->locale['p_sign_posn'] : $lang->locale['n_sign_posn']; $positiveNegativeSymbolFormatLength = strlen( $positiveNegativeSymbolFormat ); for ( $i=0; $i < $positiveNegativeSymbolFormatLength; $i++ ) { switch ( $positiveNegativeSymbolFormat[ $i ] ) { case 0: if ( $currencySymbolPreceeds ) { $currencySymbol = "({$currencySymbol}"; $amount = "{$amount})"; } else { $currencySymbol = "{$currencySymbol})"; $amount = "({$amount}"; } break; case 1: if ( $currencySymbolPreceeds ) { $currencySymbol = "{$positiveNegativeSymbol}{$currencySymbol}"; } else { $amount = "{$positiveNegativeSymbol}{$amount}"; } break; case 2: if ( $currencySymbolPreceeds ) { $amount = "{$amount}{$positiveNegativeSymbol}"; } else { $currencySymbol = "{$currencySymbol}{$positiveNegativeSymbol}"; } break; case 3: $currencySymbol = ( $positiveNegativeSymbol . $currencySymbol ); break; case 4: $currencySymbol = ( $currencySymbol . $positiveNegativeSymbol ); break; } } } return ( $currencySymbolPreceeds ? ( $currencySymbol . $spaceBetweenCurrencySymbolAndAmount . $amount ) : ( $amount . $spaceBetweenCurrencySymbolAndAmount . $currencySymbol ) ); } } /* And if all else fails, just use the currency code */ return number_format( (string) $this->amount, static::numberOfDecimalsForCurrency( $this->currency ) ) . " {$this->currency}"; } /** * Get output for API * * @param Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials * @return array * @apiresponse string currency The currency code (e.g. 'USD') * @apiresponse string amount The amount */ public function apiOutput( ?Member $authorizedMember = NULL ): array { return array( 'currency' => $this->currency, 'amount' => $this->amountAsString(), ); } }