test improvements

This commit is contained in:
Dennis Eichhorn 2020-09-02 20:39:14 +02:00
parent b084791809
commit ace6e49380
8 changed files with 79 additions and 76 deletions

View File

@ -28,8 +28,8 @@ final class CustomerValue
* Simple customer lifetime value * Simple customer lifetime value
* *
* @param float $margin Margin per period * @param float $margin Margin per period
* @param float $retentionRate Rate of remaining customers per period * @param float $retentionRate Rate of remaining customers per period (= average lifetime / (1 + average lifetime))
* @param float $discountRate Cost of capital to discound future revenue * @param float $discountRate Cost of capital to discount future revenue
* *
* @return float * @return float
* *
@ -43,30 +43,30 @@ final class CustomerValue
/** /**
* Normalized measure of recurring revenue * Normalized measure of recurring revenue
* *
* @param array $revenues Revenues * @param array $revenues Revenues
* @param int $periods Amount of revenue periods * @param int $periods Amount of revenue periods
* @param float $cutoff Normalization cutoff (which values should be ignored) * @param float $lowerCutoff Normalization cutoff (which lower values should be ignored)
* @param float $upperCutoff Normalization cutoff (which upper values should be ignored)
* *
* @return float * @return float
* *
* @since 1.0.0 * @since 1.0.0
*/ */
public static function getMRR(array $revenues, int $periods = 12, float $cutoff = 0.1) : float public static function getMRR(array $revenues, int $periods = 12, float $lowerCutoff = 0.1, float $upperCutoff = 0.0) : float
{ {
if ($cutoff === 0.0) { if ($lowerCutoff === 0.0 && $upperCutoff === 0.0) {
return \array_sum($revenues) / $periods; return \array_sum($revenues) / $periods;
} }
$count = \count($revenues); \sort($revenues);
$offset = (int) \round($count * $cutoff, 0, \PHP_ROUND_HALF_UP);
if ($offset * 2 >= $count) { $sum = 0.0;
return 0.0; foreach ($revenues as $revenue) {
if ($revenue >= $lowerCutoff && $revenue <= $upperCutoff) {
$sum += $revenue;
}
} }
\sort($revenues); return $sum / $periods;
$normalized = \array_splice($revenues, $offset, $count - $offset);
return \array_sum($normalized) / $periods;
} }
} }

View File

@ -87,46 +87,6 @@ final class Metrics
- $customers * $acquistionCost; - $customers * $acquistionCost;
} }
/**
* Life time value of a customer
*
* @param \Closure $customerProfit Profit of a customer in year t
* @param float $discountRate Discount rate
*
* @return float
*
* @since 1.0.0
*/
public static function lifeTimeValue(\Closure $customerProfit, float $discountRate) : float
{
$ltv = 0.0;
for ($i = 1; $i < 1000000; ++$i) {
$ltv += $customerProfit($i) / \pow(1 + $discountRate, $i - 1);
}
return $ltv;
}
/**
* Life time value of a customer
*
* @param \Closure $customerProfit Profit of a customer in year t
* @param float $discountRate Discount rate
*
* @return float
*
* @since 1.0.0
*/
public static function simpleRetentionLifeTimeValue(\Closure $customerProfit, float $discountRate, float $retentionRate) : float
{
$ltv = 0.0;
for ($i = 1; $i < 1000000; ++$i) {
$ltv += $customerProfit($i) * \pow($retentionRate, $i - 1) / \pow(1 + $discountRate, $i - 1);
}
return $ltv;
}
/** /**
* Calculate the profitability of customers based on their purchase behaviour * Calculate the profitability of customers based on their purchase behaviour
* *
@ -140,7 +100,7 @@ final class Metrics
* *
* @since 1.0.0 * @since 1.0.0
*/ */
public static function calclateMailingSuccess(float $discountRate, array $purchaseProbability, array $payoffs) : Matrix public static function calculateMailingSuccess(float $discountRate, array $purchaseProbability, array $payoffs) : Matrix
{ {
$count = \count($purchaseProbability); $count = \count($purchaseProbability);
$profit = new Matrix($count, $count); $profit = new Matrix($count, $count);

View File

@ -1846,7 +1846,6 @@ class DataMapperAbstract implements DataMapperInterface
} }
self::deleteModel($obj, $objId, $refClass); self::deleteModel($obj, $objId, $refClass);
self::clear(); self::clear();
return $objId; return $objId;
@ -1872,7 +1871,9 @@ class DataMapperAbstract implements DataMapperInterface
$obj = []; $obj = [];
foreach ($result as $element) { foreach ($result as $element) {
if (isset($element[static::$primaryField . '_' . $depth]) && self::isInitialized(static::class, $element[static::$primaryField . '_' . $depth], $depth)) { if (isset($element[static::$primaryField . '_' . $depth])
&& self::isInitialized(static::class, $element[static::$primaryField . '_' . $depth], $depth)
) {
$obj[$element[static::$primaryField . '_' . $depth]] = self::$initObjects[static::class][$element[static::$primaryField . '_' . $depth]['obj']]; $obj[$element[static::$primaryField . '_' . $depth]] = self::$initObjects[static::class][$element[static::$primaryField . '_' . $depth]['obj']];
continue; continue;
@ -1880,12 +1881,12 @@ class DataMapperAbstract implements DataMapperInterface
$toFill = self::createBaseModel(); $toFill = self::createBaseModel();
if (isset($element[static::$primaryField . '_' . $depth])) { if (!isset($element[static::$primaryField . '_' . $depth])) {
$obj[$element[static::$primaryField . '_' . $depth]] = self::populateAbstract($element, $toFill, $depth);
self::addInitialized(static::class, $element[static::$primaryField . '_' . $depth], $obj[$element[static::$primaryField . '_' . $depth]], $depth);
} else {
throw new \Exception(); throw new \Exception();
} }
$obj[$element[static::$primaryField . '_' . $depth]] = self::populateAbstract($element, $toFill, $depth);
self::addInitialized(static::class, $element[static::$primaryField . '_' . $depth], $obj[$element[static::$primaryField . '_' . $depth]], $depth);
} }
return $obj; return $obj;
@ -1906,18 +1907,20 @@ class DataMapperAbstract implements DataMapperInterface
$obj = []; $obj = [];
foreach ($result as $element) { foreach ($result as $element) {
if (isset($element[static::$primaryField]) && self::isInitializedArray(static::class, $element[static::$primaryField], $depth)) { if (isset($element[static::$primaryField])
&& self::isInitializedArray(static::class, $element[static::$primaryField], $depth)
) {
$obj[$element[static::$primaryField]] = self::$initArrays[static::class][$element[static::$primaryField]]['obj']; $obj[$element[static::$primaryField]] = self::$initArrays[static::class][$element[static::$primaryField]]['obj'];
continue; continue;
} }
if (isset($element[static::$primaryField])) { if (!isset($element[static::$primaryField])) {
$obj[$element[static::$primaryField]] = self::populateAbstractArray($element, [], $depth);
self::addInitializedArray(static::class, $element[static::$primaryField], $obj[$element[static::$primaryField]], $depth);
} else {
throw new \Exception(); throw new \Exception();
} }
$obj[$element[static::$primaryField]] = self::populateAbstractArray($element, [], $depth);
self::addInitializedArray(static::class, $element[static::$primaryField], $obj[$element[static::$primaryField]], $depth);
} }
return $obj; return $obj;

View File

@ -104,7 +104,7 @@ final class FileLogger implements LoggerInterface
$this->verbose = $verbose; $this->verbose = $verbose;
if (\is_dir($lpath) || \strpos($lpath, '.') === false) { if (\is_dir($lpath) || \strpos($lpath, '.') === false) {
$path = \rtrim($lpath, '/') . '/' . \date('Y-m-d') . '.log'; $path = \rtrim($path !== false ? $path : $lpath, '/') . '/' . \date('Y-m-d') . '.log';
} else { } else {
$path = $lpath; $path = $lpath;
} }

View File

@ -14,6 +14,8 @@ declare(strict_types=1);
namespace phpOMS\tests\Business\Marketing; namespace phpOMS\tests\Business\Marketing;
use phpOMS\Business\Marketing\CustomerValue;
/** /**
* @testdox phpOMS\tests\Business\Marketing\CustomerValueTest: Customer value * @testdox phpOMS\tests\Business\Marketing\CustomerValueTest: Customer value
* *
@ -22,18 +24,28 @@ namespace phpOMS\tests\Business\Marketing;
class CustomerValueTest extends \PHPUnit\Framework\TestCase class CustomerValueTest extends \PHPUnit\Framework\TestCase
{ {
/** /**
* @testdox The simple customer life time value is correctly calculated
* @group framework * @group framework
*/ */
public function testSimpleCLV() : void public function testSimpleCLV() : void
{ {
self::markTestIncomplete(); $margin = 3000;
$years = 10;
$retention = $years / (1 + $years);
self::assertEqualsWithDelta(30000, CustomerValue::getSimpleCLV($margin, $retention, 0.0), 0.1);
} }
/** /**
* @testdox The monthly recurring revenue (MRR) is correctly calculated
* @group framework * @group framework
*/ */
public function testMRR() : void public function testMRR() : void
{ {
self::markTestIncomplete(); $revenues = [
1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096
];
self::assertEqualsWithDelta(77.53846, CustomerValue::getMRR($revenues, 13, 10, 1000), 0.01);
self::assertEqualsWithDelta(630.07692307, CustomerValue::getMRR($revenues, 13, 0.0, 0.0), 0.01);
} }
} }

View File

@ -73,7 +73,7 @@ class ConnectionFactoryTest extends \PHPUnit\Framework\TestCase
} }
/** /**
* @testdox An invalid cache type results in an exception * @testdox A invalid cache type results in an exception
* @group framework * @group framework
*/ */
public function testInvalidCacheType() : void public function testInvalidCacheType() : void

View File

@ -37,9 +37,10 @@ class ModuleManagerTest extends \PHPUnit\Framework\TestCase
protected function setUp() : void protected function setUp() : void
{ {
$this->app = new class() extends ApplicationAbstract { $this->app = new class() extends ApplicationAbstract {
protected string $appName = 'Api'; protected string $appName = 'Api';
}; };
$this->app->appName = 'Api'; $this->app->appName = 'Api';
$this->app->dbPool = $GLOBALS['dbpool']; $this->app->dbPool = $GLOBALS['dbpool'];
$this->app->router = new WebRouter(); $this->app->router = new WebRouter();

View File

@ -17,19 +17,46 @@ namespace phpOMS\tests\Module;
require_once __DIR__ . '/../Autoloader.php'; require_once __DIR__ . '/../Autoloader.php';
use phpOMS\Application\ApplicationAbstract; use phpOMS\Application\ApplicationAbstract;
use phpOMS\Log\FileLogger;
use phpOMS\Module\NullModule; use phpOMS\Module\NullModule;
use phpOMS\Utils\TestUtils;
/** /**
* @testdox phpOMS\tests\Module\NullModuleTest: Basic module functionality
*
* @internal * @internal
*/ */
final class NullModuleTest extends \PHPUnit\Framework\TestCase final class NullModuleTest extends \PHPUnit\Framework\TestCase
{ {
public function testModule() : void protected NullModule $module;
protected function setUp() : void
{ {
$app = new class() extends ApplicationAbstract $app = new class() extends ApplicationAbstract
{ {
}; };
self::assertInstanceOf('\phpOMS\Module\ModuleAbstract', new NullModule($app)); $this->module = new NullModule($app);
}
/**
* @group framework
*/
public function testModule() : void
{
self::assertInstanceOf('\phpOMS\Module\ModuleAbstract', $this->module);
}
/**
* @testdox A invalid module method call will create an error log
* @covers phpOMS\Module\NullModule
* @group framework
*/
public function testInvalidModuleMethodCalls() : void
{
$this->module->invalidMethodCall();
$path = TestUtils::getMember(FileLogger::getInstance(), 'path');
self::assertStringContainsString('Expected module/controller but got NullModule.', \file_get_contents($path));
} }
} }