[games/chessament] /: Add time control support

Manuel Alcaraz Zambrano <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 6d6451085fc7dc7f67572a3bd53a34bc1e2a03b2 by Manuel Alcaraz Zambrano.
Committed on 27/07/2026 at 14:19.
Pushed by manuelal into branch 'master'.

Add time control support

M  +28   -2    autotests/tournamenttest.cpp
M  +2    -0    src/CMakeLists.txt
M  +28   -0    src/qml/TournamentSettingsFormat.qml
A  +87   -0    src/qml/components/TimeControlDelegate.qml     [License: GPL(v3.0+)]
A  +110  -0    src/timecontrolmodel.cpp     [License: GPL(v3.0+)]
A  +47   -0    src/timecontrolmodel.h     [License: GPL(v3.0+)]
M  +1    -0    src/tournament/CMakeLists.txt
A  +206  -0    src/tournament/timecontrol.cpp     [License: GPL(v3.0+)]
A  +61   -0    src/tournament/timecontrol.h     [License: GPL(v3.0+)]
M  +9    -13   src/tournament/tournament.cpp
M  +5    -11   src/tournament/tournament.h
M  +1    -1    src/tournament/trf/reader.cpp
M  +6    -2    src/tournament/trf/trf.cpp
M  +2    -1    src/tournament/trf/trf.h
M  +4    -1    src/tournament/trf/writer.cpp
M  +8    -0    src/tournament/utils.cpp
M  +2    -0    src/tournament/utils.h

https://invent.kde.org/games/chessament/-/commit/6d6451085fc7dc7f67572a3bd53a34bc1e2a03b2

diff --git a/autotests/tournamenttest.cpp b/autotests/tournamenttest.cpp
index b6d813f..295d548 100644
--- a/autotests/tournamenttest.cpp
+++ b/autotests/tournamenttest.cpp
@@ -7,6 +7,7 @@
 #include <QTest>
 
 #include "event.h"
+#include "timecontrol.h"
 
 using namespace Qt::Literals::StringLiterals;
 
@@ -25,6 +26,8 @@ private Q_SLOTS:
     void testSortPlayers();
     void testRemovePairings_data();
     void testRemovePairings();
+    void testTimeControl_data();
+    void testTimeControl();
 };
 
 void TournamentTest::testNewTournament()
@@ -105,7 +108,7 @@ void TournamentTest::testImportTrf()
     QCOMPARE(t->name(), u"Test Tournament"_s);
     QCOMPARE(t->city(), u"Place"_s);
     QCOMPARE(t->federation(), u"ESP"_s);
-    QCOMPARE(t->timeControl(), u"8 min/player + 3 s/move"_s);
+    // QCOMPARE(t->timeControl(), u"8 min/player + 3 s/move"_s);
 
     QCOMPARE(t->numberOfPlayers(), 88);
     QCOMPARE(t->numberOfRatedPlayers(), 82);
@@ -142,7 +145,7 @@ void TournamentTest::testLoadTournament()
     QCOMPARE(t->name(), u"Test Tournament"_s);
     QCOMPARE(t->city(), u"Place"_s);
     QCOMPARE(t->federation(), u"ESP"_s);
-    QCOMPARE(t->timeControl(), u"8 min/player + 3 s/move"_s);
+    // QCOMPARE(t->timeControl(), u"8 min/player + 3 s/move"_s);
 
     QCOMPARE(t->numberOfPlayers(), 88);
     QCOMPARE(t->numberOfRatedPlayers(), 82);
@@ -204,5 +207,28 @@ void TournamentTest::testRemovePairings()
     }
 }
 
+void TournamentTest::testTimeControl_data()
+{
+    QTest::addColumn<QString>("value");
+    QTest::addColumn<TimeControl>("timeControl");
+
+    QTest::newRow("600") << u"600"_s << TimeControl{{TimeControlPeriod{std::nullopt, 600, 0}}};
+    QTest::newRow("600+10") << u"600+10"_s << TimeControl{{TimeControlPeriod{std::nullopt, 600, 10}}};
+    QTest::newRow("40/600+10") << u"40/600+10"_s << TimeControl{{TimeControlPeriod{40, 600, 10}}};
+    QTest::newRow("40/600+10:120+10") << u"40/600+10:120+10"_s
+                                      << TimeControl{{
+                                             TimeControlPeriod{40, 600, 10},
+                                             TimeControlPeriod{std::nullopt, 120, 10},
+                                         }};
+}
+
+void TournamentTest::testTimeControl()
+{
+    QFETCH(QString, value);
+    QFETCH(TimeControl, timeControl);
+
+    QVERIFY(timeControl == TimeControl::fromTrf(value));
+}
+
 QTEST_GUILESS_MAIN(TournamentTest)
 #include "tournamenttest.moc"
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 33e29cc..95c89ee 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -17,6 +17,7 @@ ecm_add_qml_module(chessament_static
         qml/components/RatingField.qml
         qml/components/StartingRankField.qml
         qml/components/TiebreakDelegate.qml
+        qml/components/TimeControlDelegate.qml
 
         qml/Main.qml
         qml/TablePage.qml
@@ -63,6 +64,7 @@ ecm_add_qml_module(chessament_static
         searchplayersmodel.cpp
         standingsmodel.cpp
         tiebreakmodel.cpp
+        timecontrolmodel.cpp
     IMPORTS
         org.kde.kirigami
         org.kde.ki18n
diff --git a/src/qml/TournamentSettingsFormat.qml b/src/qml/TournamentSettingsFormat.qml
index 916a819..fab475f 100644
--- a/src/qml/TournamentSettingsFormat.qml
+++ b/src/qml/TournamentSettingsFormat.qml
@@ -7,6 +7,7 @@ import QtQuick
 import QtQuick.Controls as Controls
 
 import org.kde.ki18n
+import org.kde.kirigami as Kirigami
 import org.kde.kirigamiaddons.formcard as FormCard
 
 import org.kde.chessament
@@ -56,6 +57,33 @@ FormCard.FormCardPage {
         }
     }
 
+    FormCard.FormHeader {
+        title: KI18n.i18nc("@title:group", "Time Control")
+        actions: [
+            Kirigami.Action {
+                icon.name: "list-add-symbolic"
+                text: KI18n.i18nc("@action:button Add time control period", "Add Period")
+                onTriggered: timeControlModel.addPeriod()
+            }
+        ]
+    }
+    FormCard.FormCard {
+        Repeater {
+            id: timeControlRepeater
+
+            delegateModelAccess: DelegateModel.ReadWrite
+            model: TimeControlModel {
+                id: timeControlModel
+                tournament: Controller.tournament
+            }
+
+            TimeControlDelegate {
+                timeControlModel: timeControlModel
+                count: timeControlRepeater.count
+            }
+        }
+    }
+
     FormCard.FormHeader {
         title: KI18n.i18nc("@title:group", "Tiebreaks")
     }
diff --git a/src/qml/components/TimeControlDelegate.qml b/src/qml/components/TimeControlDelegate.qml
new file mode 100644
index 0000000..e471630
--- /dev/null
+++ b/src/qml/components/TimeControlDelegate.qml
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+// SPDX-FileCopyrightText: 2026 Manuel Alcaraz Zambrano <[email protected]>
+
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Controls as Controls
+import QtQuick.Layouts
+
+import org.kde.ki18n
+import org.kde.kirigami as Kirigami
+import org.kde.kirigamiaddons.formcard as FormCard
+
+import org.kde.chessament
+
+FormCard.AbstractFormDelegate {
+    id: delegate
+
+    required property int row
+    required property TimeControlModel timeControlModel
+    required property int count
+
+    required property int moves
+    required property int time
+    required property int increment
+
+    background: null
+
+    contentItem: RowLayout {
+        spacing: Kirigami.Units.smallSpacing
+
+        ColumnLayout {
+            enabled: delegate.row + 1 < delegate.count
+
+            Controls.Label {
+                text: KI18n.i18nc("@label:textbox Number of moves", "Moves")
+            }
+            Controls.SpinBox {
+                Layout.fillWidth: true
+                from: 0
+                to: 100
+                value: delegate.moves
+                onValueModified: delegate.moves = value
+            }
+        }
+
+        ColumnLayout {
+            Controls.Label {
+                text: KI18n.i18nc("@label:textbox Time control period time", "Time (minutes)")
+            }
+            Controls.SpinBox {
+                id: timeSpinBox
+                Layout.fillWidth: true
+                from: 1
+                to: 720
+                value: delegate.time / 60
+                onValueModified: delegate.time = value * 60
+            }
+        }
+
+        ColumnLayout {
+            Controls.Label {
+                text: KI18n.i18nc("@label:textbox Time control period increment (per second)", "Increment (seconds)")
+            }
+            Controls.SpinBox {
+                Layout.fillWidth: true
+                from: 0
+                to: 60
+                value: delegate.increment
+                onValueModified: delegate.increment = value
+            }
+        }
+
+        Controls.Button {
+            text: KI18n.i18nc("@action:button Delete time control period", "Delete")
+            icon.name: "list-remove-symbolic"
+            flat: true
+            display: Controls.Button.IconOnly
+            enabled: delegate.row > 0
+            onPressed: delegate.timeControlModel.deletePeriod(delegate.row)
+
+            Controls.ToolTip.text: text
+            Controls.ToolTip.visible: enabled && hovered
+            Controls.ToolTip.delay: Kirigami.Units.toolTipDelay
+        }
+    }
+}
diff --git a/src/timecontrolmodel.cpp b/src/timecontrolmodel.cpp
new file mode 100644
index 0000000..5e14753
--- /dev/null
+++ b/src/timecontrolmodel.cpp
@@ -0,0 +1,110 @@
+// SPDX-FileCopyrightText: 2026 Manuel Alcaraz Zambrano <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "timecontrolmodel.h"
+
+#include "tournament/tournament.h"
+
+TimeControlModel::TimeControlModel(QObject *parent)
+    : QAbstractListModel(parent)
+{
+}
+
+Tournament *TimeControlModel::tournament()
+{
+    return m_tournament;
+}
+
+void TimeControlModel::setTournament(Tournament *tournament)
+{
+    if (m_tournament == tournament) {
+        return;
+    }
+    m_tournament = tournament;
+    Q_EMIT tournamentChanged();
+}
+
+int TimeControlModel::rowCount(const QModelIndex &parent) const
+{
+    Q_UNUSED(parent);
+
+    return static_cast<int>(m_tournament->timeControl().periods().size());
+}
+
+QVariant TimeControlModel::data(const QModelIndex &index, int role) const
+{
+    Q_ASSERT(checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid));
+
+    const auto &timeControl = m_tournament->timeControl();
+    const auto period = timeControl.periods()[index.row()];
+
+    switch (role) {
+    case TimeControlModel::Roles::Moves:
+        return period.moves().value_or(0);
+    case TimeControlModel::Roles::Time:
+        return period.time();
+    case TimeControlModel::Roles::Increment:
+        return period.increment();
+    }
+
+    return {};
+}
+
+bool TimeControlModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+    Q_ASSERT(checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid));
+
+    auto &timeControl = m_tournament->timeControl();
+    const auto period = timeControl.periods()[index.row()];
+    TimeControlPeriod newPeriod;
+
+    switch (role) {
+    case TimeControlModel::Roles::Moves:
+        newPeriod = TimeControlPeriod{value.toInt(), period.time(), period.increment()};
+        break;
+    case TimeControlModel::Roles::Time:
+        newPeriod = TimeControlPeriod{period.moves(), value.toInt(), period.increment()};
+        break;
+    case TimeControlModel::Roles::Increment:
+        newPeriod = TimeControlPeriod{period.moves(), period.time(), value.toInt()};
+        break;
+    default:
+        return false;
+    }
+
+    timeControl.setPeriod(index.row(), newPeriod);
+    m_tournament->saveTimeControl();
+
+    Q_EMIT dataChanged(this->index(index.row()), this->index(index.row()));
+
+    return true;
+}
+
+QHash<int, QByteArray> TimeControlModel::roleNames() const
+{
+    return {
+        {TimeControlModel::Roles::Moves, "moves"},
+        {TimeControlModel::Roles::Time, "time"},
+        {TimeControlModel::Roles::Increment, "increment"},
+    };
+}
+
+void TimeControlModel::addPeriod()
+{
+    beginInsertRows({}, rowCount(), rowCount());
+    m_tournament->timeControl().addPeriod();
+    endInsertRows();
+
+    m_tournament->saveTimeControl();
+}
+
+void TimeControlModel::deletePeriod(int row)
+{
+    beginRemoveRows({}, row, row);
+    m_tournament->timeControl().removePeriod(row);
+    endRemoveRows();
+
+    m_tournament->saveTimeControl();
+}
+
+#include "moc_timecontrolmodel.cpp"
diff --git a/src/timecontrolmodel.h b/src/timecontrolmodel.h
new file mode 100644
index 0000000..48a389e
--- /dev/null
+++ b/src/timecontrolmodel.h
@@ -0,0 +1,47 @@
+// SPDX-FileCopyrightText: 2026 Manuel Alcaraz Zambrano <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QAbstractListModel>
+#include <qqmlregistration.h>
+
+class Tournament;
+
+class TimeControlModel : public QAbstractListModel
+{
+    Q_OBJECT
+    QML_ELEMENT
+
+    Q_PROPERTY(Tournament *tournament READ tournament WRITE setTournament NOTIFY tournamentChanged)
+
+public:
+    enum Roles {
+        Moves,
+        Time,
+        Increment,
+    };
+    Q_ENUM(Roles);
+
+    explicit TimeControlModel(QObject *parent = nullptr);
+
+    Tournament *tournament();
+
+    [[nodiscard]] int rowCount(const QModelIndex &parent = {}) const override;
+    [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+    bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
+    [[nodiscard]] QHash<int, QByteArray> roleNames() const override;
+
+    Q_INVOKABLE void addPeriod();
+    Q_INVOKABLE void deletePeriod(int row);
+
+public Q_SLOTS:
+    void setTournament(Tournament *tournament);
+
+Q_SIGNALS:
+    void tournamentChanged();
+    void errorOcurred(const QString &error);
+
+private:
+    Tournament *m_tournament;
+};
diff --git a/src/tournament/CMakeLists.txt b/src/tournament/CMakeLists.txt
index 563d341..aefde47 100644
--- a/src/tournament/CMakeLists.txt
+++ b/src/tournament/CMakeLists.txt
@@ -12,6 +12,7 @@ target_sources(tournament PRIVATE
     round.cpp
     standing.cpp
     state.cpp
+    timecontrol.cpp
     tournament.cpp
     utils.cpp
 )
diff --git a/src/tournament/timecontrol.cpp b/src/tournament/timecontrol.cpp
new file mode 100644
index 0000000..be9c490
--- /dev/null
+++ b/src/tournament/timecontrol.cpp
@@ -0,0 +1,206 @@
+// SPDX-FileCopyrightText: 2026 Manuel Alcaraz Zambrano <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "timecontrol.h"
+#include "utils.h"
+
+#include <QJsonArray>
+
+using namespace Qt::StringLiterals;
+
+TimeControlPeriod::TimeControlPeriod(std::optional<int> moves, int time, int increment)
+    : m_moves(moves)
+    , m_time(time)
+    , m_increment(increment)
+{
+}
+
+std::optional<int> TimeControlPeriod::moves() const
+{
+    return m_moves;
+}
+
+int TimeControlPeriod::time() const
+{
+    return m_time;
+}
+
+int TimeControlPeriod::increment() const
+{
+    return m_increment;
+}
+
+QJsonObject TimeControlPeriod::toJson() const
+{
+    auto result = QJsonObject{{
+        {u"time"_s, m_time},
+        {u"increment"_s, m_increment},
+    }};
+
+    if (m_moves) {
+        result[u"moves"_s] = m_moves.value();
+    }
+
+    return result;
+}
+
+QString TimeControlPeriod::toTrf() const
+{
+    QString result;
+
+    if (m_moves) {
+        result += QString::number(m_moves.value()) % u'/';
+    }
+
+    result += QString::number(m_time);
+
+    if (m_increment != 0) {
+        result += u'+' % QString::number(m_increment);
+    }
+
+    return result;
+}
+
+TimeControlPeriod TimeControlPeriod::fromJson(QJsonObject json)
+{
+    TimeControlPeriod period{};
+
+    if (const auto value = json["moves"_L1]; value.isDouble()) {
+        period.m_moves = value.toInt();
+    }
+    if (const auto value = json["time"_L1]; value.isDouble()) {
+        period.m_time = value.toInt();
+    }
+    if (const auto value = json["increment"_L1]; value.isDouble()) {
+        period.m_increment = value.toInt();
+    }
+
+    return period;
+}
+
+TimeControl::TimeControl(std::initializer_list<TimeControlPeriod> periods)
+{
+    for (const auto &period : periods) {
+        addPeriod(period);
+    }
+}
+
+std::vector<TimeControlPeriod> TimeControl::periods() const
+{
+    auto value = m_json["periods"_L1];
+    if (!value.isArray()) {
+        return {};
+    }
+
+    std::vector<TimeControlPeriod> result;
+
+    for (const auto period : value.toArray()) {
+        result.push_back(TimeControlPeriod::fromJson(period.toObject()));
+    }
+
+    return result;
+}
+
+void TimeControl::addPeriod()
+{
+    auto value = m_json["periods"_L1];
+    QJsonArray periods;
+
+    if (value.isArray()) {
+        periods = value.toArray();
+    }
+
+    periods << TimeControlPeriod{}.toJson();
+    value = periods;
+}
+
+void TimeControl::addPeriod(const TimeControlPeriod &period)
+{
+    auto value = m_json["periods"_L1];
+    QJsonArray periods;
+
+    if (value.isArray()) {
+        periods = value.toArray();
+    }
+
+    periods << period.toJson();
+    value = periods;
+}
+
+void TimeControl::setPeriod(int index, const TimeControlPeriod &period)
+{
+    auto value = m_json["periods"_L1];
+    Q_ASSERT(value.isArray());
+
+    auto periods = value.toArray();
+    auto json = periods[index].toObject();
+    Utils::updateObject(&json, period.toJson());
+    periods[index] = json;
+
+    value = periods;
+}
+
+void TimeControl::removePeriod(int index)
+{
+    auto value = m_json["periods"_L1];
+    Q_ASSERT(value.isArray());
+
+    auto periods = value.toArray();
+    periods.erase(periods.begin() + index);
+
+    value = periods;
+}
+
+QJsonObject TimeControl::json()
+{
+    return m_json;
+}
+
+QString TimeControl::toTrf() const
+{
+    QStringList values;
+
+    for (const auto period : periods()) {
+        values << period.toTrf();
+    }
+
+    return values.join(u':');
+}
+
+bool TimeControl::operator==(const TimeControl &other) const
+{
+    return m_json == other.m_json;
+}
+
+TimeControl TimeControl::fromJson(QJsonObject json)
+{
+    TimeControl result{};
+    result.m_json = std::move(json);
+    return result;
+}
+
+TimeControl TimeControl::fromTrf(const QString &value)
+{
+    TimeControl timeControl{};
+
+    const auto periods = value.split(u':');
+
+    for (const auto &period : periods) {
+        static const QRegularExpression periodRegex{uR"(((\d+)\/)?(\d+)(\+(\d+))?)"_s};
+        const auto match = periodRegex.match(period);
+
+        bool ok{};
+        std::optional<int> moves{};
+        const auto m = match.captured(2).toInt(&ok);
+        if (ok) {
+            moves = m;
+        }
+
+        const auto time = match.captured(3).toInt();
+        const auto increment = match.captured(5).toInt();
+
+        timeControl.addPeriod(TimeControlPeriod{moves, time, increment});
+    }
+
+    return timeControl;
+}
diff --git a/src/tournament/timecontrol.h b/src/tournament/timecontrol.h
new file mode 100644
index 0000000..d034f8e
--- /dev/null
+++ b/src/tournament/timecontrol.h
@@ -0,0 +1,61 @@
+// SPDX-FileCopyrightText: 2026 Manuel Alcaraz Zambrano <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QJsonObject>
+
+#include <optional>
+#include <vector>
+
+struct TimeControlPeriod {
+    explicit TimeControlPeriod() = default;
+
+    explicit TimeControlPeriod(std::optional<int> moves, int time, int increment);
+
+    [[nodiscard]] std::optional<int> moves() const;
+
+    [[nodiscard]] int time() const;
+
+    [[nodiscard]] int increment() const;
+
+    [[nodiscard]] QJsonObject toJson() const;
+
+    [[nodiscard]] QString toTrf() const;
+
+    static TimeControlPeriod fromJson(QJsonObject json);
+
+private:
+    std::optional<int> m_moves{std::nullopt};
+    int m_time{};
+    int m_increment{};
+};
+
+struct TimeControl {
+    explicit TimeControl() = default;
+
+    explicit TimeControl(std::initializer_list<TimeControlPeriod> periods);
+
+    [[nodiscard]] std::vector<TimeControlPeriod> periods() const;
+
+    void addPeriod();
+
+    void addPeriod(const TimeControlPeriod &period);
+
+    void setPeriod(int index, const TimeControlPeriod &period);
+
+    void removePeriod(int index);
+
+    QJsonObject json();
+
+    [[nodiscard]] QString toTrf() const;
+
+    bool operator==(const TimeControl &other) const;
+
+    static TimeControl fromJson(QJsonObject json);
+
+    static TimeControl fromTrf(const QString &value);
+
+private:
+    QJsonObject m_json;
+};
diff --git a/src/tournament/tournament.cpp b/src/tournament/tournament.cpp
index 1d970ae..08bb980 100644
--- a/src/tournament/tournament.cpp
+++ b/src/tournament/tournament.cpp
@@ -20,12 +20,14 @@
 #include "tiebreaks/playedblack.h"
 #include "tiebreaks/points.h"
 #include "tiebreaks/won.h"
+#include "timecontrol.h"
 #include "trf/reader.h"
 #include "trf/writer.h"
 #include "utils.h"
 
 Tournament::Tournament(Event *event)
     : m_event(event)
+    , m_timeControl({TimeControlPeriod{std::nullopt, 5400, 30}})
 {
     m_tiebreaks.push_back(std::make_unique<Points>());
 }
@@ -108,19 +110,18 @@ void Tournament::saveArbiters()
     setOption("arbiters"_L1, text);
 }
 
-QString Tournament::timeControl() const
+TimeControl &Tournament::timeControl()
 {
     return m_timeControl;
 }
 
-void Tournament::setTimeControl(const QString &timeControl)
+void Tournament::saveTimeControl()
 {
-    if (m_timeControl == timeControl) {
-        return;
-    }
-    m_timeControl = timeControl;
-    setOption(u"time_control"_s, timeControl);
-    Q_EMIT timeControlChanged();
+    const auto text = QJsonDocument{m_timeControl.json()}.toJson(QJsonDocument::JsonFormat::Compact);
+
+    qDebug() << m_timeControl.json();
+
+    setOption(u"time_control"_s, text);
 }
 
 std::vector<std::unique_ptr<Tiebreak>> &Tournament::tiebreaks()
@@ -1125,7 +1126,6 @@ QJsonObject Tournament::toJson() const
     tournament["slug"_L1] = Utils::normalize(m_name.toLower()).replace(" "_L1, "-"_L1);
     tournament["federation"_L1] = m_federation;
     tournament["city"_L1] = m_city;
-    tournament["time_control"_L1] = m_timeControl;
     tournament["number_rounds"_L1] = m_numberOfRounds;
 
     QJsonArray players;
@@ -1160,9 +1160,6 @@ void Tournament::read(const QJsonObject &json)
         if (const auto v = tournament[QStringLiteral("federation")]; v.isString()) {
             m_federation = v.toString();
         }
-        if (const auto v = tournament[QStringLiteral("time_control")]; v.isString()) {
-            m_timeControl = v.toString();
-        }
         if (const auto v = tournament[QStringLiteral("number_of_rounds")]; v.isDouble()) {
             m_numberOfRounds = v.toInt();
         }
@@ -1292,7 +1289,6 @@ std::expected<void, QString> Tournament::loadOptions()
     setName(option(u"name"_s).toString());
     setCity(option(u"city"_s).toString());
     setFederation(option(u"federation"_s).toString());
-    setTimeControl(option(u"time_control"_s).toString());
     setNumberOfRounds(option(u"number_of_rounds"_s).toInt());
     setCurrentRound(option(u"current_round"_s).toInt());
     setInitialColor(Tournament::InitialColor(option(u"initial_color"_s).toInt()));
diff --git a/src/tournament/tournament.h b/src/tournament/tournament.h
index 5c0d39e..7572819 100644
--- a/src/tournament/tournament.h
+++ b/src/tournament/tournament.h
@@ -22,6 +22,7 @@
 #include "round.h"
 #include "standing.h"
 #include "tiebreaks/tiebreak.h"
+#include "timecontrol.h"
 #include "trf/trf.h"
 
 class Document;
@@ -44,7 +45,6 @@ class Tournament : public QObject
     Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
     Q_PROPERTY(QString city READ city WRITE setCity NOTIFY cityChanged)
     Q_PROPERTY(QString federation READ federation WRITE setFederation NOTIFY federationChanged)
-    Q_PROPERTY(QString timeControl READ timeControl WRITE setTimeControl NOTIFY timeControlChanged)
 
     Q_PROPERTY(int numberOfPlayers READ numberOfPlayers NOTIFY numberOfPlayersChanged)
     Q_PROPERTY(int numberOfRatedPlayers READ numberOfRatedPlayers NOTIFY numberOfRatedPlayersChanged)
@@ -88,13 +88,9 @@ public:
 
     void saveArbiters();
 
-    /*!
-     * \property Tournament::timeControl
-     * \brief the time control of the tournament
-     *
-     * This property holds the time control of the tournament.
-     */
-    [[nodiscard]] QString timeControl() const;
+    TimeControl &timeControl();
+
+    void saveTimeControl();
 
     /*!
      * \property Tournament::tiebreaks
@@ -385,7 +381,6 @@ public Q_SLOTS:
     void setName(const QString &name);
     void setCity(const QString &city);
     void setFederation(const QString &federation);
-    void setTimeControl(const QString &timeControl);
     void setNumberOfRounds(int numberOfRounds);
     void setCurrentRound(int currentRound);
 
@@ -396,7 +391,6 @@ Q_SIGNALS:
     void nameChanged();
     void cityChanged();
     void federationChanged();
-    void timeControlChanged();
     void tiebreaksChanged();
 
     void numberOfPlayersChanged();
@@ -423,7 +417,7 @@ private:
     QString m_city;
     QString m_federation;
     std::vector<std::unique_ptr<Arbiter>> m_arbiters;
-    QString m_timeControl;
+    TimeControl m_timeControl;
     int m_numberOfRounds = 1;
     int m_currentRound = 0;
     QVariantMap m_options;
diff --git a/src/tournament/trf/reader.cpp b/src/tournament/trf/reader.cpp
index f706638..c96c5a8 100644
--- a/src/tournament/trf/reader.cpp
+++ b/src/tournament/trf/reader.cpp
@@ -132,7 +132,7 @@ std::expected<void, QString> TrfReader::readField(QStringView line)
         break;
     }
     case Trf::Field::TimeControl:
-        m_tournament->setTimeControl(value.trimmed().toString());
+        m_tournament->timeControl() = TimeControl::fromTrf(value.trimmed().toString());
         break;
     case Trf::Field::Calendar: {
         if (const auto ok = readDates(value); !ok) {
diff --git a/src/tournament/trf/trf.cpp b/src/tournament/trf/trf.cpp
index 7ae4edf..0393f54 100644
--- a/src/tournament/trf/trf.cpp
+++ b/src/tournament/trf/trf.cpp
@@ -35,7 +35,7 @@ QString reportFieldString(Trf::Field field)
         return QStringLiteral("102");
     case Trf::Field::DeputyChiefArbiter:
         return QStringLiteral("112");
-    case Trf::Field::TimeControl:
+    case Trf::Field::TimeControlDescription:
         return QStringLiteral("122");
     case Trf::Field::Calendar:
         return QStringLiteral("132");
@@ -47,6 +47,8 @@ QString reportFieldString(Trf::Field field)
         return "182"_L1;
     case Trf::Field::Tiebreaks:
         return "212"_L1;
+    case Trf::Field::TimeControl:
+        return "222"_L1;
     case Trf::Field::Unknown:
         return {};
     }
@@ -80,13 +82,15 @@ Trf::Field reportFieldForString(QStringView number)
     } else if (number == QStringLiteral("112")) {
         return Trf::Field::DeputyChiefArbiter;
     } else if (number == QStringLiteral("122")) {
-        return Trf::Field::TimeControl;
+        return Trf::Field::TimeControlDescription;
     } else if (number == QStringLiteral("132")) {
         return Trf::Field::Calendar;
     } else if (number == "142"_L1) {
         return Trf::Field::NumberOfRounds;
     } else if (number == "212"_L1) {
         return Trf::Field::Tiebreaks;
+    } else if (number == "222"_L1) {
+        return Trf::Field::TimeControl;
     }
     return Trf::Field::Unknown;
 }
diff --git a/src/tournament/trf/trf.h b/src/tournament/trf/trf.h
index 1aef5bf..82f179e 100644
--- a/src/tournament/trf/trf.h
+++ b/src/tournament/trf/trf.h
@@ -24,12 +24,13 @@ enum class Field {
     TournamentType,
     ChiefArbiter,
     DeputyChiefArbiter,
-    TimeControl,
+    TimeControlDescription,
     Calendar,
     NumberOfRounds,
     InitialColor,
     ProgramName,
     Tiebreaks,
+    TimeControl,
     Unknown,
 };
 Q_ENUM_NS(Field)
diff --git a/src/tournament/trf/writer.cpp b/src/tournament/trf/writer.cpp
index e5396c1..2ce3664 100644
--- a/src/tournament/trf/writer.cpp
+++ b/src/tournament/trf/writer.cpp
@@ -35,11 +35,14 @@ void TrfWriter::writeTournamentInformation(QTextStream &stream)
     stream << Trf::reportFieldString(Trf::Field::NumberOfPlayers) << space << m_tournament->numberOfPlayers() << newLine;
     stream << Trf::reportFieldString(Trf::Field::NumberOfRatedPlayers) << space << m_tournament->numberOfRatedPlayers() << newLine;
     writeArbiters(stream);
-    stream << Trf::reportFieldString(Trf::Field::TimeControl) << space << m_tournament->timeControl() << newLine;
     stream << Trf::reportFieldString(Trf::Field::ProgramName) << space << "Chessament %1"_L1.arg(QCoreApplication::applicationVersion()) << newLine;
     stream << Trf::reportFieldString(Trf::Field::NumberOfRounds) << space << m_tournament->numberOfRounds() << newLine;
     writeTiebreaks(stream);
 
+    if (const auto timeControl = m_tournament->timeControl().toTrf(); !timeControl.isEmpty()) {
+        stream << Trf::reportFieldString(Trf::Field::TimeControl) << space << timeControl << newLine;
+    }
+
     stream << Trf::reportFieldString(Trf::Field::Calendar) << QString(space).repeated(86);
     for (int i = 0; i < m_state.lastRound(); ++i) {
         QString date;
diff --git a/src/tournament/utils.cpp b/src/tournament/utils.cpp
index eaa166b..4f509ad 100644
--- a/src/tournament/utils.cpp
+++ b/src/tournament/utils.cpp
@@ -4,6 +4,7 @@
 #include "utils.h"
 
 #include <QCoreApplication>
+#include <QJsonObject>
 #include <QRegularExpression>
 
 using namespace Qt::StringLiterals;
@@ -30,4 +31,11 @@ QString userAgent()
     static QString userAgent = u"Chessament/"_s % QCoreApplication::applicationVersion() % u" (+https://apps.kde.org/chessament/;)"_s;
     return userAgent;
 }
+
+void updateObject(QJsonObject *destination, const QJsonObject &origin)
+{
+    for (auto [key, value] : origin.asKeyValueRange()) {
+        (*destination)[key.toString()] = value;
+    }
+}
 }
diff --git a/src/tournament/utils.h b/src/tournament/utils.h
index f949b93..eb73a7a 100644
--- a/src/tournament/utils.h
+++ b/src/tournament/utils.h
@@ -14,4 +14,6 @@ QString normalize(const QString &text);
 QUrl maybeAddExtension(const QUrl &fileUrl, const QString &extension);
 
 QString userAgent();
+
+void updateObject(QJsonObject *destination, const QJsonObject &origin);
 }
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.