Skip to content

Commit

Permalink
refactor: Optimize pass rate calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
pan93412 committed Oct 4, 2024
1 parent ab31326 commit 08aaee6
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 65 deletions.
1 change: 1 addition & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ services:
- "../src/Entity/"
- "../src/Kernel.php"
- "../src/Service/Processes/"
- "../src/Service/Types/"
- "../src/Twig/Components/Challenge/EventConstant.php"

# add more service definitions when explicit configuration is needed
Expand Down
49 changes: 0 additions & 49 deletions src/Entity/Question.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,55 +176,6 @@ public function setSolutionVideo(?string $solution_video): static
return $this;
}

/**
* Get the pass rate of the question.
*
* @param Group|null $group the group to filter the attempts by (null = no group)
*
* @return float the pass rate of the question
*/
public function getPassRate(?Group $group): float
{
$totalAttemptCount = $this->getTotalAttemptCount($group);
if (0 === $totalAttemptCount) {
return 0;
}

return round($this->getTotalSolvedCount($group) / $totalAttemptCount * 100, 2);
}

/**
* Get the total number of attempts made on the question.
*
* @param Group|null $group the group to filter the attempts by (null = no group)
*
* @return int the total number of attempts made on the question
*/
public function getTotalAttemptCount(?Group $group): int
{
return $this->getSolutionEvents()
->filter(fn (SolutionEvent $solutionEvent) => $group === $solutionEvent->getSubmitter()?->getGroup())
->count();
}

/**
* Get the total number of times the question has been solved.
*
* @param Group|null $group the group to filter the attempts by (null = no group)
*
* @return int the total number of times the question has been solved
*/
public function getTotalSolvedCount(?Group $group): int
{
return $this->getSolutionEvents()
->filter(
fn (SolutionEvent $solutionEvent) => (
SolutionEventStatus::Passed === $solutionEvent->getStatus()
&& $group === $solutionEvent->getSubmitter()?->getGroup()
)
)->count();
}

/**
* @return Collection<int, SolutionEvent>
*/
Expand Down
30 changes: 30 additions & 0 deletions src/Repository/SolutionEventRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,34 @@ public function listLeaderboard(?Group $group, string $interval): array

return $leaderboard;
}

/**
* Get the total attempts made on the question.
*
* @param Question $question the question to query
* @param Group|null $group the group to filter the attempts by (null = no group)
*
* @return SolutionEvent[] the total attempts made on the question
*/
public function getTotalAttempts(Question $question, ?Group $group): array
{
$qb = $this->createQueryBuilder('se')
->join('se.submitter', 'submitter')
->where('se.question = :question')
->setParameter('question', $question);

if ($group) {
$qb->andWhere('submitter.group = :group')
->setParameter('group', $group);
} else {
$qb->andWhere('submitter.group IS NULL');
}

/**
* @var SolutionEvent[] $result
*/
$result = $qb->getQuery()->getResult();

return $result;
}
}
36 changes: 36 additions & 0 deletions src/Service/PassRateService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace App\Service;

use App\Entity\Group;
use App\Entity\Question;
use App\Repository\SolutionEventRepository;
use App\Service\Types\PassRate;

/**
* Get the pass rate of a question in the optimized matter.
*/
readonly class PassRateService
{
public function __construct(
private SolutionEventRepository $solutionEventRepository,
) {
}

/**
* Get the pass rate in this group of a question.
*
* @param Question $question the question to calculate the pass rate
* @param Group|null $group the group to calculate the pass rate, null for no group
*
* @return PassRate the pass rate, see {@link PassRate} for details
*/
public function getPassRate(Question $question, ?Group $group): PassRate
{
$attempts = $this->solutionEventRepository->getTotalAttempts($question, $group);

return new PassRate($attempts);
}
}
67 changes: 67 additions & 0 deletions src/Service/Types/PassRate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace App\Service\Types;

use App\Entity\SolutionEvent;
use App\Entity\SolutionEventStatus;

/**
* The pass rate of a question.
*
* - `total`: the total number of attempts
* - `passed`: the number of successful attempts
* - `passRate`: the pass rate of the question in percentage
* - `level`: the level of the pass rate, can be 'low', 'medium', or 'high'
*/
readonly class PassRate
{
/**
* @var int the total number of attempts
*/
public int $total;

/**
* @var int the number of successful attempts
*/
public int $passed;

/**
* @param SolutionEvent[] $attempts
*/
public function __construct(
array $attempts,
) {
$this->total = \count($attempts);
$this->passed = \count(array_filter($attempts, fn (SolutionEvent $event) => SolutionEventStatus::Passed == $event->getStatus()));
}

/**
* Calculate the pass rate of a question.
*
* @return float the pass rate of the question in percentage
*/
public function getPassRate(): float
{
if (0 === $this->total) {
return 0;
}

return round($this->passed / $this->total * 100, 2);
}

/**
* @return string the level of the pass rate, can be 'low', 'medium', or 'high'
*/
public function getLevel(): string
{
$passRate = $this->getPassRate();

return match (true) {
$passRate <= 40 => 'low',
$passRate <= 70 => 'medium',
default => 'high',
};
}
}
8 changes: 8 additions & 0 deletions src/Twig/Components/Challenge/Header.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
use App\Entity\User;
use App\Repository\QuestionRepository;
use App\Repository\SolutionEventRepository;
use App\Service\PassRateService;
use App\Service\Types\PassRate;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;

#[AsTwigComponent]
Expand All @@ -21,6 +23,7 @@ final class Header
public function __construct(
private readonly SolutionEventRepository $solutionEventRepository,
private readonly QuestionRepository $questionRepository,
private readonly PassRateService $passRateService,
) {
}

Expand All @@ -42,4 +45,9 @@ public function getPreviousPage(): ?int
{
return $this->questionRepository->getPreviousPage($this->question->getId());
}

public function getPassRate(): PassRate
{
return $this->passRateService->getPassRate($this->question, $this->user->getGroup());
}
}
23 changes: 10 additions & 13 deletions src/Twig/Components/Questions/Card.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use App\Entity\Question;
use App\Entity\User;
use App\Service\PassRateService;
use App\Service\Types\PassRate;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;

#[AsTwigComponent]
Expand All @@ -14,21 +16,16 @@ final class Card
public Question $question;
public User $currentUser;

public function __construct(
private readonly PassRateService $passRateService,
) {
}

/**
* Get the pass rate level of the question.
*
* Low: 0% - 40%
* Medium: 41 – 70%
* High: 71% - 100%
* Get the pass rate of the question.
*/
public function getPassRateLevel(): string
public function getPassRate(): PassRate
{
$passRate = $this->question->getPassRate($this->currentUser->getGroup());

return match (true) {
$passRate <= 40 => 'low',
$passRate <= 70 => 'medium',
default => 'high',
};
return $this->passRateService->getPassRate($this->question, $this->currentUser->getGroup());
}
}
5 changes: 3 additions & 2 deletions templates/components/Challenge/Header.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
<li class="challenge-header__lists__type">{{ question.type }}</li>
<li class="challenge-header__lists__pass_rate">
通過率
{% set passRate = this.passRate %}
<a class="challenge-header__lists__pass_rate__value" href="#" rel="help" data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-title="{{ question.totalAttemptCount(user.group) }} 次挑戰中有 {{ question.totalSolvedCount(user.group) }} 次成功">
{{ question.passRate(user.group) }}%
data-bs-title="{{ passRate.total }} 次挑戰中有 {{ passRate.passed }} 次成功">
{{ passRate.passRate }}%
</a>
</li>
</ul>
Expand Down
3 changes: 2 additions & 1 deletion templates/components/Questions/Card.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@

<div class="question-card__operations">
<a role="button" class="btn btn-primary" href="{{ path('app_challenge', {id: question.id}) }}">進行測驗</a>
<div class="question-card__pass-rate" data-pass-rate="{{ this.passRateLevel }}">通過率 {{ question.passRate(currentUser.group) }}%</div>
{% set passRate = this.passRate %}
<div class="question-card__pass-rate" data-pass-rate="{{ passRate.level }}">通過率 {{ passRate.passRate }}%</div>
</div>

<div class="question-card__background">
Expand Down

0 comments on commit 08aaee6

Please sign in to comment.