added new rounding mode: closest (#611)

This commit is contained in:
Kevin Papst
2019-03-03 20:33:35 +01:00
committed by GitHub
parent bea4be881b
commit 642db480ce
11 changed files with 554 additions and 73 deletions

View File

@@ -19,13 +19,14 @@ kimai:
# Rounding rules are used to round the begin & end dates and the duration for timesheet records.
# The "default" rule will round "begin" down and "end" up to the full minute, the "duration" will not be rounded.
# Please read var/docs/configurations.md to find out more about rounding rules
# Find out more about rounding rules at https://www.kimai.org/documentation/timesheet.html
rounding:
default:
days: ['monday','tuesday','wednesday','thursday','friday','saturday','sunday']
begin: 1
end: 1
duration: 0
mode: default
# If you want to apply different hourly rates for specific weekdays, you can uncomment the "rates" configuration.
# The "weekend" rule will add 50% to each timesheet entry that will be recorded on "saturdays" or "sundays".
@@ -69,7 +70,7 @@ kimai:
# currency: EUR
# --------------------------------------------------------------------------------
# Free configurable permission system, see var/docs/permissions.md
# Find out more about the configurable permission system at https://www.kimai.org/documentation/permissions.html
permissions:
sets:
# mapping complex rulesets of single permissions to named "sets" ("set name" = [array of "permissions"])
@@ -186,7 +187,7 @@ kimai:
end: '20:00'
# You can configure unlimited google calendars to display events for your company (e.g. holidays)
# Please read var/docs/configurations.md to find out more about calendar integration
# Find out more about calendar integration at https://www.kimai.org/documentation/calendar.html
# google:
# api_key: 'your-restricted-google-api-key'
# sources:
@@ -235,7 +236,7 @@ kimai:
widgets: [amountToday, amountWeek, amountMonth, amountYear]
# --------------------------------------------------------------------------------
# All available widgets, please see documentation at var/docs/dashboard.md
# Find out more about dashboard widgets at https://www.kimai.org/documentation/dashboard.html
widgets:
userDurationToday: { title: stats.durationToday, query: duration, user: true, begin: '00:00:00', end: '23:59:59', icon: duration, color: green }
userDurationWeek: { title: stats.durationWeek, query: duration, user: true, begin: 'monday this week 00:00:00', end: 'sunday this week 23:59:59', icon: duration, color: blue }

View File

@@ -11,6 +11,7 @@ namespace App\DependencyInjection;
use App\Model\DashboardSection;
use App\Model\Widget;
use App\Timesheet\Rounding\RoundingInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
@@ -81,6 +82,21 @@ class Configuration implements ConfigurationInterface
->integerNode('duration')
->defaultValue(0)
->end()
->scalarNode('mode')
->defaultValue('default')
->validate()
->thenInvalid('Chosen rounding mode is invalid')
->ifTrue(function ($value) {
$class = 'App\\Timesheet\\Rounding\\' . ucfirst($value) . 'Rounding';
if (class_exists($class)) {
$rounding = new $class();
return !($rounding instanceof RoundingInterface);
}
return false;
})
->thenInvalid('Chosen rounding mode is invalid')
->end()
->end()
->end()
->end()
->defaultValue([])

View File

@@ -11,6 +11,7 @@ namespace App\Timesheet\Calculator;
use App\Entity\Timesheet;
use App\Timesheet\CalculatorInterface;
use App\Timesheet\Rounding\RoundingInterface;
/**
* Implementation to calculate the durations for a timesheet record.
@@ -66,74 +67,14 @@ class DurationCalculator implements CalculatorInterface
$days = array_map('strtolower', $rounding['days']);
if (in_array(strtolower($weekday), $days)) {
$this->roundBegin($record, $rounding['begin']);
$this->roundEnd($record, $rounding['end']);
$class = 'App\\Timesheet\\Rounding\\' . ucfirst($rounding['mode']) . 'Rounding';
/* @var $rounder RoundingInterface */
$rounder = new $class();
$rounder->roundBegin($record, $rounding['begin']);
$rounder->roundEnd($record, $rounding['end']);
$this->applyDuration($record);
$this->roundDuration($record, $rounding['duration']);
$rounder->roundDuration($record, $rounding['duration']);
}
}
}
/**
* @param Timesheet $record
* @param int $minutes
*/
protected function roundBegin(Timesheet $record, $minutes)
{
if ($minutes <= 0) {
return;
}
$timestamp = $record->getBegin()->getTimestamp();
$seconds = $minutes * 60;
$diff = $timestamp % $seconds;
if (0 === $diff) {
return;
}
$record->getBegin()->setTimestamp($timestamp - $diff);
}
/**
* @param Timesheet $record
* @param int $minutes
*/
protected function roundEnd(Timesheet $record, $minutes)
{
if ($minutes <= 0) {
return;
}
$timestamp = $record->getEnd()->getTimestamp();
$seconds = $minutes * 60;
$diff = $timestamp % $seconds;
if (0 === $diff) {
return;
}
$record->getEnd()->setTimestamp($timestamp - $diff + $seconds);
}
/**
* @param Timesheet $record
* @param int $minutes
*/
protected function roundDuration(Timesheet $record, $minutes)
{
if ($minutes <= 0) {
return;
}
$timestamp = $record->getDuration();
$seconds = $minutes * 60;
$diff = $timestamp % $seconds;
if (0 === $diff) {
return;
}
$record->setDuration($timestamp - $diff + $seconds);
}
}

View File

@@ -0,0 +1,90 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Timesheet\Rounding;
use App\Entity\Timesheet;
class ClosestRounding implements RoundingInterface
{
/**
* @param Timesheet $record
* @param int $minutes
*/
public function roundBegin(Timesheet $record, $minutes)
{
if ($minutes <= 0) {
return;
}
$timestamp = $record->getBegin()->getTimestamp();
$seconds = $minutes * 60;
$diff = $timestamp % $seconds;
if (0 === $diff) {
return;
}
if ($diff > ($seconds / 2)) {
$record->getBegin()->setTimestamp($timestamp - $diff + $seconds);
} else {
$record->getBegin()->setTimestamp($timestamp - $diff);
}
}
/**
* @param Timesheet $record
* @param int $minutes
*/
public function roundEnd(Timesheet $record, $minutes)
{
if ($minutes <= 0) {
return;
}
$timestamp = $record->getEnd()->getTimestamp();
$seconds = $minutes * 60;
$diff = $timestamp % $seconds;
if (0 === $diff) {
return;
}
if ($diff > ($seconds / 2)) {
$record->getEnd()->setTimestamp($timestamp - $diff + $seconds);
} else {
$record->getEnd()->setTimestamp($timestamp - $diff);
}
}
/**
* @param Timesheet $record
* @param int $minutes
*/
public function roundDuration(Timesheet $record, $minutes)
{
if ($minutes <= 0) {
return;
}
$timestamp = $record->getDuration();
$seconds = $minutes * 60;
$diff = $timestamp % $seconds;
if (0 === $diff) {
return;
}
if ($diff > ($seconds / 2)) {
$record->setDuration($timestamp - $diff + $seconds);
} else {
$record->setDuration($timestamp - $diff);
}
}
}

View File

@@ -0,0 +1,78 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Timesheet\Rounding;
use App\Entity\Timesheet;
class DefaultRounding implements RoundingInterface
{
/**
* @param Timesheet $record
* @param int $minutes
*/
public function roundBegin(Timesheet $record, $minutes)
{
if ($minutes <= 0) {
return;
}
$timestamp = $record->getBegin()->getTimestamp();
$seconds = $minutes * 60;
$diff = $timestamp % $seconds;
if (0 === $diff) {
return;
}
$record->getBegin()->setTimestamp($timestamp - $diff);
}
/**
* @param Timesheet $record
* @param int $minutes
*/
public function roundEnd(Timesheet $record, $minutes)
{
if ($minutes <= 0) {
return;
}
$timestamp = $record->getEnd()->getTimestamp();
$seconds = $minutes * 60;
$diff = $timestamp % $seconds;
if (0 === $diff) {
return;
}
$record->getEnd()->setTimestamp($timestamp - $diff + $seconds);
}
/**
* @param Timesheet $record
* @param int $minutes
*/
public function roundDuration(Timesheet $record, $minutes)
{
if ($minutes <= 0) {
return;
}
$timestamp = $record->getDuration();
$seconds = $minutes * 60;
$diff = $timestamp % $seconds;
if (0 === $diff) {
return;
}
$record->setDuration($timestamp - $diff + $seconds);
}
}

View File

@@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Timesheet\Rounding;
use App\Entity\Timesheet;
/**
* Apply rounding rules to the given timesheet.
*/
interface RoundingInterface
{
/**
* @param Timesheet $record
* @param int $minutes
*/
public function roundBegin(Timesheet $record, $minutes);
/**
* @param Timesheet $record
* @param int $minutes
*/
public function roundEnd(Timesheet $record, $minutes);
/**
* @param Timesheet $record
* @param $minutes
*/
public function roundDuration(Timesheet $record, $minutes);
}

View File

@@ -28,8 +28,8 @@
'username': 'hidden-xs',
'email': 'hidden-xs hidden-sm',
'title': 'hidden-xs',
'active': '',
'roles': '',
'active': '',
'actions': 'alwaysVisible',
} %}
@@ -43,12 +43,12 @@
<td class="{{ tables.data_table_column_class(tableName, columns, 'username') }}">{{ entry.username }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'email') }}">{{ entry.email }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'title') }}">{{ entry.title }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'active') }}">{{ widgets.label_visible(entry.enabled) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'roles') }}">
{% for role in entry.roles %}
{{ widgets.label_role(role) }}
{% endfor %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'active') }}">{{ widgets.label_visible(entry.enabled) }}</td>
<td>
{% set actionButtons = {} %}
{% if is_granted('edit', entry) %}

View File

@@ -10,7 +10,7 @@
<li>
<a href="{{ linkUserProfile }}">
<div class="pull-left image">
{{ macro.avatar(user.avatar, user.username, 'img-circle') }}
{{ macro.avatar(user.avatar, user.username, 'img-circle') }}
</div>
<div class="menu-info">
<h4 class="control-sidebar-subheading">{{ widgets.username(user) }}</h4>

View File

@@ -64,6 +64,7 @@ class DurationCalculatorTest extends TestCase
'begin' => 15,
'end' => 15,
'duration' => 0,
'mode' => 'default',
],
],
(clone $start)->setTime(12, 17, 35),
@@ -77,6 +78,7 @@ class DurationCalculatorTest extends TestCase
'begin' => 0,
'end' => 0,
'duration' => 0,
'mode' => 'default',
],
],
(clone $start)->setTime(12, 17, 35),
@@ -90,6 +92,7 @@ class DurationCalculatorTest extends TestCase
'begin' => 1,
'end' => 1,
'duration' => 0,
'mode' => 'default',
],
],
(clone $start)->setTime(12, 17, 35),
@@ -103,6 +106,7 @@ class DurationCalculatorTest extends TestCase
'begin' => 0,
'end' => 0,
'duration' => 30,
'mode' => 'default',
],
],
(clone $start)->setTime(12, 10, 51),
@@ -116,12 +120,14 @@ class DurationCalculatorTest extends TestCase
'begin' => 15,
'end' => 0,
'duration' => 0,
'mode' => 'default',
],
'weekdays' => [
'days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
'begin' => 0,
'end' => 1,
'duration' => 30,
'mode' => 'default',
],
],
(clone $start)->setTime(12, 27, 35), // 12:15
@@ -135,12 +141,14 @@ class DurationCalculatorTest extends TestCase
'begin' => 15,
'end' => 0,
'duration' => 30,
'mode' => 'default',
],
'weekdays' => [
'days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
'begin' => 0,
'end' => 1,
'duration' => 0,
'mode' => 'default',
],
],
(clone $start)->setTime(12, 27, 35), // 12:15
@@ -154,12 +162,14 @@ class DurationCalculatorTest extends TestCase
'begin' => 0,
'end' => 0,
'duration' => 1,
'mode' => 'default',
],
'weekdays' => [
'days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
'begin' => 0,
'end' => 0,
'duration' => 1,
'mode' => 'default',
],
],
(clone $start)->setTime(12, 27, 35), // no diff, to test ...
@@ -173,12 +183,14 @@ class DurationCalculatorTest extends TestCase
'begin' => 1,
'end' => 1,
'duration' => 1,
'mode' => 'default',
],
'weekdays' => [
'days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
'begin' => 1,
'end' => 1,
'duration' => 1,
'mode' => 'default',
],
],
(clone $start)->setTime(12, 27, 00), // no diff, to test ...
@@ -192,12 +204,14 @@ class DurationCalculatorTest extends TestCase
'begin' => 0,
'end' => 0,
'duration' => 0,
'mode' => 'default',
],
'weekdays' => [
'days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
'begin' => 0,
'end' => 0,
'duration' => 0,
'mode' => 'default',
],
],
(clone $start)->setTime(12, 27, 35), // no diff, to test ...

View File

@@ -0,0 +1,152 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Timesheet\Calculator;
use App\Entity\Timesheet;
use App\Timesheet\Rounding\ClosestRounding;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Timesheet\Rounding\ClosestRounding
*/
class ClosestRoundingTest extends TestCase
{
/**
* @dataProvider getTestData
*/
public function testCalculate($roundBegin, $roundEnd, $roundDuration, \DateTime $start, \DateTime $end, \DateTime $expectedStart, \DateTime $expectedEnd, $expectedDuration)
{
$record = new Timesheet();
$record->setBegin($start);
$record->setEnd($end);
$this->assertEquals(0, $record->getDuration());
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
$sut = new ClosestRounding();
$sut->roundBegin($record, $roundBegin);
$sut->roundEnd($record, $roundEnd);
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
$sut->roundDuration($record, $roundDuration);
$this->assertEquals($expectedStart->getTimestamp(), $record->getBegin()->getTimestamp());
$this->assertEquals($expectedEnd->getTimestamp(), $record->getEnd()->getTimestamp());
$this->assertEquals($expectedDuration, $record->getDuration());
}
public function getTestData()
{
$start = new \DateTime();
$start->setTime(12, 0, 0);
return [
[
0,
0,
0,
$start,
(clone $start)->setTimestamp($start->getTimestamp() + 1837),
$start,
(clone $start)->setTimestamp($start->getTimestamp() + 1837),
1837
],
[
15,
15,
0,
(clone $start)->setTime(12, 17, 35),
(clone $start)->setTime(13, 32, 52),
(clone $start)->setTime(12, 15, 00),
(clone $start)->setTime(13, 30, 00),
4500
],
[
0,
0,
0,
(clone $start)->setTime(12, 17, 35),
(clone $start)->setTime(13, 32, 52),
(clone $start)->setTime(12, 17, 35),
(clone $start)->setTime(13, 32, 52),
4517
],
[
1,
1,
0,
(clone $start)->setTime(12, 17, 35),
(clone $start)->setTime(13, 32, 52),
(clone $start)->setTime(12, 18, 00),
(clone $start)->setTime(13, 33, 00),
4500
],
[
0,
0,
30,
(clone $start)->setTime(12, 10, 51),
(clone $start)->setTime(14, 40, 52),
(clone $start)->setTime(12, 10, 51),
(clone $start)->setTime(14, 40, 52),
9000
],
[
0,
1,
30,
(clone $start)->setTime(12, 27, 35), // 12:15
(clone $start)->setTime(14, 32, 52), // 14:33 => 2:18 => 2:30
(clone $start)->setTime(12, 27, 35),
(clone $start)->setTime(14, 33, 00),
7200
],
[
15,
0,
30,
(clone $start)->setTime(12, 27, 35), // 12:15
(clone $start)->setTime(14, 32, 52), // 14:33 => 2:18 (second duration will not be rounded)
(clone $start)->setTime(12, 30, 00), // 12:15
(clone $start)->setTime(14, 32, 52), // 14:33 => 2:18 (second duration will not be rounded)
7200
],
[
0,
0,
1,
(clone $start)->setTime(12, 27, 35), // no diff, to test ...
(clone $start)->setTime(12, 27, 35), // ... that no rounding is applied
(clone $start)->setTime(12, 27, 35), // no diff, to test ...
(clone $start)->setTime(12, 27, 35), // ... that no rounding is applied
0
],
[
1,
1,
1,
(clone $start)->setTime(12, 27, 00), // no diff, to test ...
(clone $start)->setTime(12, 27, 00), // ... that no rounding is applied
(clone $start)->setTime(12, 27, 00), // no diff, to test ...
(clone $start)->setTime(12, 27, 00), // ... that no rounding is applied
0
],
[
0,
0,
0,
(clone $start)->setTime(12, 27, 35), // no diff, to test ...
(clone $start)->setTime(12, 27, 35), // ... that no rounding is applied
(clone $start)->setTime(12, 27, 35), // no diff, to test ...
(clone $start)->setTime(12, 27, 35), // ... that no rounding is applied
0
],
];
}
}

View File

@@ -0,0 +1,152 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Timesheet\Calculator;
use App\Entity\Timesheet;
use App\Timesheet\Rounding\DefaultRounding;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Timesheet\Rounding\DefaultRounding
*/
class DefaultRoundingTest extends TestCase
{
/**
* @dataProvider getTestData
*/
public function testCalculate($roundBegin, $roundEnd, $roundDuration, \DateTime $start, \DateTime $end, \DateTime $expectedStart, \DateTime $expectedEnd, $expectedDuration)
{
$record = new Timesheet();
$record->setBegin($start);
$record->setEnd($end);
$this->assertEquals(0, $record->getDuration());
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
$sut = new DefaultRounding();
$sut->roundBegin($record, $roundBegin);
$sut->roundEnd($record, $roundEnd);
$record->setDuration($record->getEnd()->getTimestamp() - $record->getBegin()->getTimestamp());
$sut->roundDuration($record, $roundDuration);
$this->assertEquals($expectedStart->getTimestamp(), $record->getBegin()->getTimestamp());
$this->assertEquals($expectedEnd->getTimestamp(), $record->getEnd()->getTimestamp());
$this->assertEquals($expectedDuration, $record->getDuration());
}
public function getTestData()
{
$start = new \DateTime();
$start->setTime(12, 0, 0);
return [
[
0,
0,
0,
$start,
(clone $start)->setTimestamp($start->getTimestamp() + 1837),
$start,
(clone $start)->setTimestamp($start->getTimestamp() + 1837),
1837
],
[
15,
15,
0,
(clone $start)->setTime(12, 17, 35),
(clone $start)->setTime(13, 32, 52),
(clone $start)->setTime(12, 15, 00),
(clone $start)->setTime(13, 45, 00),
5400
],
[
0,
0,
0,
(clone $start)->setTime(12, 17, 35),
(clone $start)->setTime(13, 32, 52),
(clone $start)->setTime(12, 17, 35),
(clone $start)->setTime(13, 32, 52),
4517
],
[
1,
1,
0,
(clone $start)->setTime(12, 17, 35),
(clone $start)->setTime(13, 32, 52),
(clone $start)->setTime(12, 17, 00),
(clone $start)->setTime(13, 33, 00),
4560
],
[
0,
0,
30,
(clone $start)->setTime(12, 10, 51),
(clone $start)->setTime(14, 40, 52),
(clone $start)->setTime(12, 10, 51),
(clone $start)->setTime(14, 40, 52),
10800
],
[
0,
1,
30,
(clone $start)->setTime(12, 27, 35), // 12:15
(clone $start)->setTime(14, 32, 52), // 14:33 => 2:18 => 2:30
(clone $start)->setTime(12, 27, 35),
(clone $start)->setTime(14, 33, 00),
9000
],
[
15,
0,
30,
(clone $start)->setTime(12, 27, 35), // 12:15
(clone $start)->setTime(14, 32, 52), // 14:33 => 2:18 (second duration will not be rounded)
(clone $start)->setTime(12, 15, 00), // 12:15
(clone $start)->setTime(14, 32, 52), // 14:33 => 2:18 (second duration will not be rounded)
9000
],
[
0,
0,
1,
(clone $start)->setTime(12, 27, 35), // no diff, to test ...
(clone $start)->setTime(12, 27, 35), // ... that no rounding is applied
(clone $start)->setTime(12, 27, 35), // no diff, to test ...
(clone $start)->setTime(12, 27, 35), // ... that no rounding is applied
0
],
[
1,
1,
1,
(clone $start)->setTime(12, 27, 00), // no diff, to test ...
(clone $start)->setTime(12, 27, 00), // ... that no rounding is applied
(clone $start)->setTime(12, 27, 00), // no diff, to test ...
(clone $start)->setTime(12, 27, 00), // ... that no rounding is applied
0
],
[
0,
0,
0,
(clone $start)->setTime(12, 27, 35), // no diff, to test ...
(clone $start)->setTime(12, 27, 35), // ... that no rounding is applied
(clone $start)->setTime(12, 27, 35), // no diff, to test ...
(clone $start)->setTime(12, 27, 35), // ... that no rounding is applied
0
],
];
}
}