Skip to content

Commit 78ccfa8

Browse files
Add commands to compare awards, scoreboard and results
1 parent 3b82b38 commit 78ccfa8

File tree

7 files changed

+501
-2
lines changed

7 files changed

+501
-2
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace App\Command;
4+
5+
use Symfony\Component\Console\Command\Command;
6+
use Symfony\Component\Console\Input\InputArgument;
7+
use Symfony\Component\Console\Input\InputInterface;
8+
use Symfony\Component\Console\Output\OutputInterface;
9+
use Symfony\Component\Console\Style\SymfonyStyle;
10+
use Symfony\Component\Serializer\SerializerInterface;
11+
12+
abstract class AbstractCompareCommand extends Command
13+
{
14+
public function __construct(protected readonly SerializerInterface $serializer)
15+
{
16+
parent::__construct();
17+
}
18+
19+
protected function configure(): void
20+
{
21+
$this
22+
->addArgument('file1', InputArgument::REQUIRED, 'First file to compare')
23+
->addArgument('file2', InputArgument::REQUIRED, 'Second file to compare');
24+
}
25+
26+
protected function execute(InputInterface $input, OutputInterface $output): int
27+
{
28+
$style = new SymfonyStyle($input, $output);
29+
$messages = [];
30+
$success = true;
31+
if (!file_exists($input->getArgument('file1'))) {
32+
$this->addMessage($messages, $success, 'error', sprintf('File "%s" does not exist', $input->getArgument('file1')));
33+
}
34+
if (!file_exists($input->getArgument('file2'))) {
35+
$this->addMessage($messages, $success, 'error', sprintf('File "%s" does not exist', $input->getArgument('file2')));
36+
}
37+
if (!$success) {
38+
return $this->displayMessages($style, $messages);
39+
}
40+
41+
$this->compare($messages, $success, $input->getArgument('file1'), $input->getArgument('file2'));
42+
43+
return $this->displayMessages($style, $messages) ?? Command::SUCCESS;
44+
}
45+
46+
/**
47+
* @param array<array{type: string, message: string, source: ?string, target: ?string}> $messages
48+
*/
49+
abstract protected function compare(
50+
array &$messages,
51+
bool &$success,
52+
string $file1,
53+
string $file2,
54+
): void;
55+
56+
/**
57+
* @param array<array{type: string, message: string, source: ?string, target: ?string}> $messages
58+
*/
59+
protected function addMessage(
60+
array &$messages,
61+
bool &$success,
62+
string $type,
63+
string $message,
64+
?string $source = null,
65+
?string $target = null,
66+
): void {
67+
$messages[] = [
68+
'type' => $type,
69+
'message' => $message,
70+
'source' => $source,
71+
'target' => $target,
72+
];
73+
if ($type === 'error') {
74+
$success = false;
75+
}
76+
}
77+
78+
/**
79+
* @param array<array{type: string, message: string, source: ?string, target: ?string}> $messages
80+
*/
81+
protected function displayMessages(SymfonyStyle $style, array $messages): ?int
82+
{
83+
if (empty($messages)) {
84+
$style->success('Files match fully');
85+
return null;
86+
}
87+
88+
$headers = ['Level', 'Message', 'Source', 'Target'];
89+
$rows = [];
90+
$counts = [];
91+
foreach ($messages as $message) {
92+
if (!isset($counts[$message['type']])) {
93+
$counts[$message['type']] = 0;
94+
}
95+
$counts[$message['type']]++;
96+
$rows[] = [
97+
$this->formatMessage($message['type'], $message['type']),
98+
$this->formatMessage($message['type'], $message['message']),
99+
$this->formatMessage($message['type'], $message['source'] ?? ''),
100+
$this->formatMessage($message['type'], $message['target'] ?? ''),
101+
];
102+
}
103+
$style->table($headers, $rows);
104+
105+
$style->newLine();
106+
foreach ($counts as $type => $count) {
107+
$style->writeln($this->formatMessage($type, sprintf('Found %d %s(s)', $count, $type)));
108+
}
109+
110+
if (isset($counts['error'])) {
111+
$style->error('Files have potential critical differences');
112+
return Command::FAILURE;
113+
}
114+
115+
$style->success('Files have differences but probably non critical');
116+
117+
return null;
118+
}
119+
120+
protected function formatMessage(string $level, string $message): string
121+
{
122+
$colors = [
123+
'error' => 'red',
124+
'warning' => 'yellow',
125+
'info' => 'green',
126+
];
127+
return sprintf('<fg=%s>%s</>', $colors[$level], $message);
128+
}
129+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace App\Command;
4+
5+
use App\DataTransferObject\Award;
6+
use Symfony\Component\Console\Attribute\AsCommand;
7+
use Symfony\Component\Serializer\Exception\ExceptionInterface;
8+
9+
#[AsCommand(
10+
name: 'compare:awards',
11+
description: 'Compare awards between two files'
12+
)]
13+
class CompareAwardsCommand extends AbstractCompareCommand
14+
{
15+
protected function compare(
16+
array &$messages,
17+
bool &$success,
18+
string $file1,
19+
string $file2,
20+
): void {
21+
try {
22+
/** @var Award[] $awards1 */
23+
$awards1 = $this->serializer->deserialize(file_get_contents($file1), Award::class . '[]', 'json');
24+
} catch (ExceptionInterface $e) {
25+
$this->addMessage($messages, $success, 'error', sprintf('Error deserializing file "%s": %s', $file1, $e->getMessage()));
26+
}
27+
try {
28+
/** @var Award[] $awards2 */
29+
$awards2 = $this->serializer->deserialize(file_get_contents($file2), Award::class . '[]', 'json');
30+
} catch (ExceptionInterface $e) {
31+
$this->addMessage($messages, $success, 'error', sprintf('Error deserializing file "%s": %s', $file2, $e->getMessage()));
32+
}
33+
34+
if (!$success || !isset($awards1) || !isset($awards2)) {
35+
return;
36+
}
37+
38+
/** @var array<string,Award> $awards1Indexed */
39+
$awards1Indexed = [];
40+
foreach ($awards1 as $award) {
41+
$awards1Indexed[$award->id] = $award;
42+
}
43+
44+
/** @var array<string,Award> $awards2Indexed */
45+
$awards2Indexed = [];
46+
foreach ($awards2 as $award) {
47+
$awards2Indexed[$award->id] = $award;
48+
}
49+
50+
foreach ($awards1Indexed as $awardId => $award) {
51+
if (!isset($awards2Indexed[$awardId])) {
52+
if (!$award->teamIds) {
53+
$this->addMessage($messages, $success, 'info', sprintf('Award "%s" not found in second file, but has no team ID\'s in first file', $awardId));
54+
} else {
55+
$this->addMessage($messages, $success, 'error', sprintf('Award "%s" not found in second file', $awardId));
56+
}
57+
} else {
58+
$award2 = $awards2Indexed[$awardId];
59+
if ($award->citation !== $award2->citation) {
60+
$this->addMessage($messages, $success, 'warning', sprintf('Award "%s" has different citation', $awardId), $award->citation, $award2->citation);
61+
}
62+
$award1TeamIds = $award->teamIds;
63+
sort($award1TeamIds);
64+
$award2TeamIds = $award2->teamIds;
65+
sort($award2TeamIds);
66+
if ($award1TeamIds !== $award2TeamIds) {
67+
$this->addMessage($messages, $success, 'error', sprintf('Award "%s" has different team ID\'s', $awardId), implode(', ', $award->teamIds), implode(', ', $award2->teamIds));
68+
}
69+
}
70+
}
71+
72+
foreach ($awards2Indexed as $awardId => $award) {
73+
if (!isset($awards1Indexed[$awardId])) {
74+
if (!$award->teamIds) {
75+
$this->addMessage($messages, $success, 'info', sprintf('Award "%s" not found in first file, but has no team ID\'s in second file', $awardId));
76+
} else {
77+
$this->addMessage($messages, $success, 'error', sprintf('Award "%s" not found in first file', $awardId));
78+
}
79+
}
80+
}
81+
}
82+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace App\Command;
4+
5+
use App\DataTransferObject\Result;
6+
use Symfony\Component\Console\Attribute\AsCommand;
7+
use Symfony\Component\Serializer\Encoder\CsvEncoder;
8+
use Symfony\Component\Serializer\Exception\ExceptionInterface;
9+
10+
#[AsCommand(
11+
name: 'compare:results',
12+
description: 'Compare results between two files'
13+
)]
14+
class CompareResultsCommand extends AbstractCompareCommand
15+
{
16+
17+
protected function compare(
18+
array &$messages,
19+
bool &$success,
20+
string $file1,
21+
string $file2,
22+
): void {
23+
$results1Contents = file_get_contents($file1);
24+
if (!str_starts_with($results1Contents, "results\t1")) {
25+
$this->addMessage($messages, $success, 'error', sprintf("File \"%s\" does not start with \"results\t1\"", $file1));
26+
}
27+
$results2Contents = file_get_contents($file2);
28+
if (!str_starts_with($results2Contents, "results\t1")) {
29+
$this->addMessage($messages, $success, 'error', sprintf("File \"%s\" does not start with \"results\t1\"", $file2));
30+
}
31+
32+
if (!$success) {
33+
return;
34+
}
35+
36+
$results1Contents = substr($results1Contents, strpos($results1Contents, "\n") + 1);
37+
$results2Contents = substr($results2Contents, strpos($results2Contents, "\n") + 1);
38+
39+
// Prefix both files with a fake header, so we can deserialize them
40+
$results1Contents = "team_id\trank\taward\tnum_solved\ttotal_time\tlast_time\tgroup_winner\n" . $results1Contents;
41+
$results2Contents = "team_id\trank\taward\tnum_solved\ttotal_time\tlast_time\tgroup_winner\n" . $results2Contents;
42+
43+
try {
44+
/** @var Result[] $results1 */
45+
$results1 = $this->serializer->deserialize($results1Contents, Result::class . '[]', 'csv', [
46+
CsvEncoder::DELIMITER_KEY => "\t",
47+
]);
48+
} catch (ExceptionInterface $e) {
49+
$this->addMessage($messages, $success, 'error', sprintf('Error deserializing file "%s": %s', $file1, $e->getMessage()));
50+
}
51+
52+
try {
53+
/** @var Result[] $results2 */
54+
$results2 = $this->serializer->deserialize($results2Contents, Result::class . '[]', 'csv', [
55+
CsvEncoder::DELIMITER_KEY => "\t",
56+
]);
57+
} catch (ExceptionInterface $e) {
58+
$this->addMessage($messages, $success, 'error', sprintf('Error deserializing file "%s": %s', $file2, $e->getMessage()));
59+
}
60+
61+
if (!$success || !isset($results1) || !isset($results2)) {
62+
return;
63+
}
64+
65+
// Sort results for both files: first by num_solved, then by total_time
66+
usort($results1, fn(
67+
Result $a,
68+
Result $b
69+
) => $a->numSolved === $b->numSolved ? $a->totalTime <=> $b->totalTime : $b->numSolved <=> $a->numSolved);
70+
usort($results2, fn(
71+
Result $a,
72+
Result $b
73+
) => $a->numSolved === $b->numSolved ? $a->totalTime <=> $b->totalTime : $b->numSolved <=> $a->numSolved);
74+
75+
/** @var array<string,Result> $results1Indexed */
76+
$results1Indexed = [];
77+
foreach ($results1 as $result) {
78+
$results1Indexed[$result->teamId] = $result;
79+
}
80+
81+
/** @var array<string,Result> $results2Indexed */
82+
$results2Indexed = [];
83+
foreach ($results2 as $result) {
84+
$results2Indexed[$result->teamId] = $result;
85+
}
86+
87+
foreach ($results1 as $result) {
88+
if (!isset($results2Indexed[$result->teamId])) {
89+
$this->addMessage($messages, $success, 'error', sprintf('Team "%s" not found in second file', $result->teamId));
90+
} else {
91+
$result2 = $results2Indexed[$result->teamId];
92+
if ($result->rank !== $result2->rank) {
93+
$this->addMessage($messages, $success, 'error', sprintf('Team %s has different rank', $result->teamId), (string)$result->rank, (string)$result2->rank);
94+
}
95+
if ($result->award !== $result2->award) {
96+
$this->addMessage($messages, $success, 'error', sprintf('Team %s has different award', $result->teamId), $result->award, $result2->award);
97+
}
98+
if ($result->numSolved !== $result2->numSolved) {
99+
$this->addMessage($messages, $success, 'error', sprintf('Team %s has different num_solved', $result->teamId), (string)$result->numSolved, (string)$result2->numSolved);
100+
}
101+
if ($result->totalTime !== $result2->totalTime) {
102+
$this->addMessage($messages, $success, 'error', sprintf('Team %s has different total_time', $result->teamId), (string)$result->totalTime, (string)$result2->totalTime);
103+
}
104+
if ($result->lastTime !== $result2->lastTime) {
105+
$this->addMessage($messages, $success, 'error', sprintf('Team %s has different last_time', $result->teamId), (string)$result->lastTime, (string)$result2->lastTime);
106+
}
107+
if ($result->groupWinner !== $result2->groupWinner) {
108+
$this->addMessage($messages, $success, 'warning', sprintf('Team %s has different group_winner', $result->teamId), (string)$result->groupWinner, (string)$result2->groupWinner);
109+
}
110+
}
111+
}
112+
113+
foreach ($results2 as $result) {
114+
if (!isset($results1Indexed[$result->teamId])) {
115+
$this->addMessage($messages, $success, 'error', sprintf('Team %s not found in first file', $result->teamId));
116+
}
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)