Skip to content

Commit

Permalink
Merge pull request #372 from HendrikPrinsZA/challenge/fx-conversion
Browse files Browse the repository at this point in the history
Fixed issue with benchmark + added FxConversion challenge + Laravel v11
  • Loading branch information
HendrikPrinsZA authored Mar 14, 2024
2 parents 3cf3be0 + de6fb45 commit e1789bf
Show file tree
Hide file tree
Showing 40 changed files with 1,601 additions and 2,028 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ version: 2.1
defaults: &defaults
resource_class: large
docker:
- image: cimg/php:8.2-node
- image: cimg/php:8.3-node
- image: cimg/mysql:8.0
environment:
MYSQL_DATABASE: "laravel"
Expand Down
3 changes: 3 additions & 0 deletions .env.circleci
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ LK_PROGRESS_BAR_DISABLED=true
LK_DD_MAX_USERS=1000
LK_DD_MAX_USER_BLOGS=10
XDEBUG_MODE=profile,trace
XDEBUG_CONFIG="output_dir=/var/www/html/storage/logs/xdebug xdebug.use_compression=false"
SAIL_XDEBUG_MODE=profile,trace
SAIL_XDEBUG_MODE=profile,trace
SAIL_XDEBUG_CONFIG="output_dir=/var/www/html/storage/logs/xdebug xdebug.use_compression=false"
EXCHANGE_RATE_API_HOST=http://127.0.0.1:8000/mock/exchangerate
EXCHANGE_RATE_API_KEY=fake-key
76 changes: 76 additions & 0 deletions app/Challenges/A/FxConversion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

namespace App\Challenges\A;

use App\Challenges\Modules\FxConversionModule;
use App\Enums\CurrencyCode;
use App\KataChallenge;
use Carbon\CarbonPeriod;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;

/**
* Test some approaches for fx conversions
*
* A. Fetch each rate from db
* B. Fetch each rate from db and cache
* C. Fetch each rate from db and cache in chunks of (month/year)
* D. Cache all rates (extreme?)
*
* General notes
* - New rates should be retained in cache for max 24hrs
*/
class FxConversion extends KataChallenge
{
protected const DATE_FROM = '2023-01-01';

protected const DATE_TO = '2024-01-01';

protected const BASE_CURRENCY_CODE = CurrencyCode::EUR;

protected const TARGET_CURRENCY_CODE = CurrencyCode::USD;

public function useScriptCache(int $iteration): float
{
Config::set('modules.fx-conversion.options.script-caching.enabled', false);
Config::set('modules.fx-conversion.options.script-caching.strategy', 'monthly');
Config::set('modules.fx-conversion.options.global-caching.enabled', false);

return $this->calculateTotalExchangeRate($iteration);
}

protected function calculateTotalExchangeRate(int $iteration, bool $useSingleton = false): float
{
$amount = 420.69;
$total = 0;

$dateFrom = Carbon::createFromFormat('Y-m-d', self::DATE_FROM);
$dateTo = $dateFrom->copy()->addDays($iteration);

$dateToMax = Carbon::createFromFormat('Y-m-d', self::DATE_TO);
if ($dateTo > $dateToMax) {
$dateTo = $dateToMax;
}

$fxConversionModule = $useSingleton ? FxConversionModule::make() : null;

$dates = [];
$carbonPeriod = CarbonPeriod::create($dateFrom, $dateTo);
foreach ($carbonPeriod as $date) {
$dates[] = $date;
$total += $useSingleton
? $fxConversionModule->convert(self::BASE_CURRENCY_CODE, self::TARGET_CURRENCY_CODE, $date, $amount)
: FxConversionModule::convert(self::BASE_CURRENCY_CODE, self::TARGET_CURRENCY_CODE, $date, $amount);
}

// No do it in reverse
while (! empty($dates)) {
$date = array_pop($dates);
$total += $useSingleton
? $fxConversionModule->convert(self::BASE_CURRENCY_CODE, self::TARGET_CURRENCY_CODE, $date, $amount)
: FxConversionModule::convert(self::BASE_CURRENCY_CODE, self::TARGET_CURRENCY_CODE, $date, $amount);
}

return $total;
}
}
18 changes: 18 additions & 0 deletions app/Challenges/B/FxConversion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace App\Challenges\B;

use App\Challenges\A\FxConversion as AFxConversion;
use Illuminate\Support\Facades\Config;

class FxConversion extends AFxConversion
{
public function useScriptCache(int $iteration): float
{
Config::set('modules.fx-conversion.options.script-caching.enabled', true);
Config::set('modules.fx-conversion.options.script-caching.strategy', 'monthly');
Config::set('modules.fx-conversion.options.global-caching.enabled', false);

return $this->calculateTotalExchangeRate($iteration, true);
}
}
111 changes: 111 additions & 0 deletions app/Challenges/Modules/FxConversionModule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

namespace App\Challenges\Modules;

use App\Enums\CurrencyCode;
use App\Models\Currency;
use App\Models\ExchangeRate;
use Clockwork\Support\Laravel\Facade;
use Exception;
use Illuminate\Support\Carbon;

class FxConversionModule extends Facade
{
protected static ?FxConversionModule $instance = null;

protected static array $cachedRates = [];

public static function make(): self
{
self::$instance ??= new self();

return self::$instance;
}

public static function convert(
CurrencyCode $baseCurrencyCode,
CurrencyCode $targetCurrencyCode,
Carbon $date,
float $amount
): float {
return $amount * self::getRate($baseCurrencyCode, $targetCurrencyCode, $date);
}

public static function getRate(
CurrencyCode $baseCurrencyCode,
CurrencyCode $targetCurrencyCode,
Carbon $date
): float {
if ($baseCurrencyCode === $targetCurrencyCode) {
return 1;
}

$baseCurrency = $baseCurrencyCode->getModel();
$targetCurrency = $targetCurrencyCode->getModel();

if (config('modules.fx-conversion.options.script-caching.enabled')) {
$cacheKey = sprintf('%d:%d:%s', $baseCurrency->id, $targetCurrency->id, $date->toDateString());

if (isset(self::$cachedRates[$cacheKey]['rate'])) {
return self::$cachedRates[$cacheKey]['rate'];
}
}

return self::getRateFromDatabase($baseCurrency, $targetCurrency, $date);
}

protected static function getRateFromDatabase(
Currency $baseCurrency,
Currency $targetCurrency,
Carbon $date
): float {
$dateString = $date->toDateString();

$exchangeRates = ExchangeRate::query()
->select('rate', 'date')
->where('base_currency_id', $baseCurrency->id)
->where('target_currency_id', $targetCurrency->id);

$exchangeRates = match (config('modules.fx-conversion.options.script-caching.strategy')) {
'daily' => $exchangeRates->where('date', $dateString),
'monthly' => $exchangeRates->whereBetween('date', [
$date->copy()->startOfMonth()->subMonth(),
$date->copy()->addMonth()->endOfMonth(),
]),
'yearly' => $exchangeRates->whereBetween('date', [
$date->copy()->startOfMonth()->subMonth(),
$date->copy()->addMonth()->endOfMonth(),
]),
'all' => $exchangeRates,
};

$exchangeRates = $exchangeRates->get()->mapWithKeys(fn (ExchangeRate $exchangeRate) => [
sprintf('%d:%d:%s', $baseCurrency->id, $targetCurrency->id, $exchangeRate->date->format('Y-m-d')) => [
'rate' => $exchangeRate->rate,
'monthly_rate_open' => $exchangeRate->monthly_rate_open,
'monthly_rate_average' => $exchangeRate->monthly_rate_average,
'monthly_rate_close' => $exchangeRate->monthly_rate_close,
],
])->toArray();

$actualExchangeRateKey = sprintf('%d:%d:%s', $baseCurrency->id, $targetCurrency->id, $date->toDateString());
$actualExchangeRate = $exchangeRates[$actualExchangeRateKey] ?? null;
if (is_null($actualExchangeRate)) {
throw new Exception(sprintf(
'No exchange rate found for %s to %s on %s',
$baseCurrency->code->value,
$targetCurrency->code->value,
$dateString
));
}

if (config('modules.fx-conversion.options.script-caching.enabled')) {
self::$cachedRates = [
...self::$cachedRates,
...$exchangeRates,
];
}

return $actualExchangeRate['rate'];
}
}
101 changes: 101 additions & 0 deletions app/Challenges/Modules/FxConversionModule.php.bck
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

namespace App\Challenges\Modules;

use App\Enums\CurrencyCode;
use App\Models\Currency;
use App\Models\ExchangeRate;
use Exception;
use Illuminate\Support\Carbon;

class FxConversionModule
{
protected static array $cachedRates = [];

public static function convert(
CurrencyCode $baseCurrencyCode,
CurrencyCode $targetCurrencyCode,
Carbon $date,
float $amount
): float {
return $amount * self::getRate($baseCurrencyCode, $targetCurrencyCode, $date);
}

public static function getRate(
CurrencyCode $baseCurrencyCode,
CurrencyCode $targetCurrencyCode,
Carbon $date
): float {
if ($baseCurrencyCode === $targetCurrencyCode) {
return 1;
}

$baseCurrency = $baseCurrencyCode->getModel();
$targetCurrency = $targetCurrencyCode->getModel();

if (config('modules.fx-conversion.options.script-caching.enabled')) {
$cacheKey = sprintf('%d:%d:%s', $baseCurrency->id, $targetCurrency->id, $date->toDateString());

if (isset(self::$cachedRates[$cacheKey]['rate'])) {
return self::$cachedRates[$cacheKey]['rate'];
}
}

return self::getRateFromDatabase($baseCurrency, $targetCurrency, $date);
}

protected static function getRateFromDatabase(
Currency $baseCurrency,
Currency $targetCurrency,
Carbon $date
): float {
$dateString = $date->toDateString();

$exchangeRates = ExchangeRate::query()
->select('rate', 'date')
->where('base_currency_id', $baseCurrency->id)
->where('target_currency_id', $targetCurrency->id);

$exchangeRates = match (config('modules.fx-conversion.options.script-caching.strategy')) {
'daily' => $exchangeRates->where('date', $dateString),
'monthly' => $exchangeRates->whereBetween('date', [
$date->copy()->startOfMonth()->subMonth(),
$date->copy()->addMonth()->endOfMonth(),
]),
'yearly' => $exchangeRates->whereBetween('date', [
$date->copy()->startOfMonth()->subMonth(),
$date->copy()->addMonth()->endOfMonth(),
]),
'all' => $exchangeRates,
};

$exchangeRates = $exchangeRates->get()->mapWithKeys(fn (ExchangeRate $exchangeRate) => [
sprintf('%d:%d:%s', $baseCurrency->id, $targetCurrency->id, $exchangeRate->date->format('Y-m-d')) => [
'rate' => $exchangeRate->rate,
'monthly_rate_open' => $exchangeRate->monthly_rate_open,
'monthly_rate_average' => $exchangeRate->monthly_rate_average,
'monthly_rate_close' => $exchangeRate->monthly_rate_close,
],
])->toArray();

$actualExchangeRateKey = sprintf('%d:%d:%s', $baseCurrency->id, $targetCurrency->id, $date->toDateString());
$actualExchangeRate = $exchangeRates[$actualExchangeRateKey] ?? null;
if (is_null($actualExchangeRate)) {
throw new Exception(sprintf(
'No exchange rate found for %s to %s on %s',
$baseCurrency->code->value,
$targetCurrency->code->value,
$dateString
));
}

if (config('modules.fx-conversion.options.script-caching.enabled')) {
self::$cachedRates = [
...self::$cachedRates,
...$exchangeRates,
];
}

return $actualExchangeRate['rate'];
}
}
9 changes: 8 additions & 1 deletion app/Console/Commands/KataRunCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,14 @@ protected function handleRun(array $challenges = []): int
throw $exception;
}

$this->line(wrap_in_format(sprintf('%s', $exception->getMessage()), false));
$warning = sprintf('%s', $exception->getMessage());
if (in_array($exception::class, config('laravel-kata.ignore-exceptions'))) {
$this->warn($warning);

return self::SUCCESS;
}

$this->error($warning);

return self::FAILURE;
}
Expand Down
6 changes: 6 additions & 0 deletions app/Enums/CurrencyCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Enums;

use App\Models\Currency;
use App\Traits\EnumTrait;

enum CurrencyCode: string
Expand All @@ -13,4 +14,9 @@ enum CurrencyCode: string
case GBP = 'GBP';
case USD = 'USD';
case ZAR = 'ZAR';

public function getModel(): Currency
{
return Currency::query()->where('code', $this->value)->first();
}
}
7 changes: 7 additions & 0 deletions app/Exceptions/KataChallengeScoreOutputsMd5Exception.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace App\Exceptions;

class KataChallengeScoreOutputsMd5Exception extends KataChallengeScoreException
{
}
Loading

0 comments on commit e1789bf

Please sign in to comment.