. */ declare(strict_types=1); namespace FireflyIII\Support; use Crypt; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\UserGroup; use FireflyIII\User; use Illuminate\Support\Collection; use NumberFormatter; /** * Class Amount. * */ class Amount { /** * This method will properly format the given number, in color or "black and white", * as a currency, given two things: the currency required and the current locale. * * @param TransactionCurrency $format * @param string $amount * @param bool $coloured * * @return string * @throws FireflyException */ public function formatAnything(TransactionCurrency $format, string $amount, bool $coloured = null): string { return $this->formatFlat($format->symbol, (int)$format->decimal_places, $amount, $coloured); } /** * This method will properly format the given number, in color or "black and white", * as a currency, given two things: the currency required and the current locale. * * @param string $symbol * @param int $decimalPlaces * @param string $amount * @param bool $coloured * * @return string * * @throws FireflyException */ public function formatFlat(string $symbol, int $decimalPlaces, string $amount, bool $coloured = null): string { $locale = app('steam')->getLocale(); $rounded = app('steam')->bcround($amount, $decimalPlaces); $coloured = $coloured ?? true; $fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY); $fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $symbol); $fmt->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimalPlaces); $fmt->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimalPlaces); $result = $fmt->format((float)$rounded); // intentional float if (true === $coloured) { if (1 === bccomp($rounded, '0')) { return sprintf('%s', $result); } if (-1 === bccomp($rounded, '0')) { return sprintf('%s', $result); } return sprintf('%s', $result); } return $result; } /** * @return Collection */ public function getAllCurrencies(): Collection { return TransactionCurrency::orderBy('code', 'ASC')->get(); } /** * @return Collection */ public function getCurrencies(): Collection { /** @var User $user */ $user = auth()->user(); return $user->currencies()->orderBy('code', 'ASC')->get(); } /** * @return TransactionCurrency * @throws FireflyException */ public function getDefaultCurrency(): TransactionCurrency { /** @var User $user */ $user = auth()->user(); return $this->getDefaultCurrencyByUserGroup($user->userGroup); } /** * @param User $user * * @return TransactionCurrency * @deprecated use getDefaultCurrencyByUserGroup instead. */ public function getDefaultCurrencyByUser(User $user): TransactionCurrency { return $this->getDefaultCurrencyByUserGroup($user->userGroup); } /** * @param UserGroup $userGroup * * @return TransactionCurrency */ public function getDefaultCurrencyByUserGroup(UserGroup $userGroup): TransactionCurrency { $cache = new CacheProperties(); $cache->addProperty('getDefaultCurrencyByGroup'); $cache->addProperty($userGroup->id); if ($cache->has()) { return $cache->get(); } $default = $userGroup->currencies()->where('group_default', true)->first(); if (null === $default) { $default = $this->getSystemCurrency(); // could be the user group has no default right now. $userGroup->currencies()->sync([$default->id => ['group_default' => true]]); } $cache->store($default); return $default; } /** * @return TransactionCurrency */ public function getSystemCurrency(): TransactionCurrency { return TransactionCurrency::where('code', 'EUR')->first(); } /** * This method returns the correct format rules required by accounting.js, * the library used to format amounts in charts. * * Used only in one place. * * @return array * @throws FireflyException */ public function getJsConfig(): array { $config = $this->getLocaleInfo(); $negative = self::getAmountJsConfig($config['n_sep_by_space'], $config['n_sign_posn'], $config['negative_sign'], $config['n_cs_precedes']); $positive = self::getAmountJsConfig($config['p_sep_by_space'], $config['p_sign_posn'], $config['positive_sign'], $config['p_cs_precedes']); return [ 'mon_decimal_point' => $config['mon_decimal_point'], 'mon_thousands_sep' => $config['mon_thousands_sep'], 'format' => [ 'pos' => $positive, 'neg' => $negative, 'zero' => $positive, ], ]; } /** * @return array * @throws FireflyException */ private function getLocaleInfo(): array { // get config from preference, not from translation: $locale = app('steam')->getLocale(); $array = app('steam')->getLocaleArray($locale); setlocale(LC_MONETARY, $array); $info = localeconv(); // correct variables $info['n_cs_precedes'] = $this->getLocaleField($info, 'n_cs_precedes'); $info['p_cs_precedes'] = $this->getLocaleField($info, 'p_cs_precedes'); $info['n_sep_by_space'] = $this->getLocaleField($info, 'n_sep_by_space'); $info['p_sep_by_space'] = $this->getLocaleField($info, 'p_sep_by_space'); $fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY); $info['mon_decimal_point'] = $fmt->getSymbol(NumberFormatter::MONETARY_SEPARATOR_SYMBOL); $info['mon_thousands_sep'] = $fmt->getSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL); return $info; } /** * @param array $info * @param string $field * * @return bool */ private function getLocaleField(array $info, string $field): bool { return (is_bool($info[$field]) && true === $info[$field]) || (is_int($info[$field]) && 1 === $info[$field]); } /** * bool $sepBySpace is $localeconv['n_sep_by_space'] * int $signPosn = $localeconv['n_sign_posn'] * string $sign = $localeconv['negative_sign'] * bool $csPrecedes = $localeconv['n_cs_precedes']. * * @param bool $sepBySpace * @param int $signPosn * @param string $sign * @param bool $csPrecedes * * @return string * */ public static function getAmountJsConfig(bool $sepBySpace, int $signPosn, string $sign, bool $csPrecedes): string { // negative first: $space = ' '; // require space between symbol and amount? if (false === $sepBySpace) { $space = ''; // no } // there are five possible positions for the "+" or "-" sign (if it is even used) // pos_a and pos_e could be the ( and ) symbol. $posA = ''; // before everything $posB = ''; // before currency symbol $posC = ''; // after currency symbol $posD = ''; // before amount $posE = ''; // after everything // format would be (currency before amount) // AB%sC_D%vE // or: // AD%v_B%sCE (amount before currency) // the _ is the optional space // switch on how to display amount: switch ($signPosn) { default: case 0: // ( and ) around the whole thing $posA = '('; $posE = ')'; break; case 1: // The sign string precedes the quantity and currency_symbol $posA = $sign; break; case 2: // The sign string succeeds the quantity and currency_symbol $posE = $sign; break; case 3: // The sign string immediately precedes the currency_symbol $posB = $sign; break; case 4: // The sign string immediately succeeds the currency_symbol $posC = $sign; } // default is amount before currency $format = $posA . $posD . '%v' . $space . $posB . '%s' . $posC . $posE; if ($csPrecedes) { // alternative is currency before amount $format = $posA . $posB . '%s' . $posC . $space . $posD . '%v' . $posE; } return $format; } }