diff --git a/mo/util.py b/mo/util.py
index 2a817c4569dbc820dd63b4a8b288e3b52631e1dd..6a021e248173600258f58b696a6c5e2a35aa39a6 100644
--- a/mo/util.py
+++ b/mo/util.py
@@ -1,12 +1,13 @@
 # Různé
 
+from dataclasses import dataclass
 import datetime
 import email.message
 import email.headerregistry
 import re
-from sqlalchemy.orm import joinedload
 import subprocess
-from typing import Any, Optional
+import sys
+from typing import Any, Optional, Noreturn
 import textwrap
 
 import mo.db as db
@@ -64,3 +65,30 @@ def send_password_reset_email(user: db.User, link: str):
 
     if sm.returncode != 0:
         raise RuntimeError('Sendmail failed with return code {}'.format(sm.returncode))
+
+
+def die(msg: str) -> Noreturn:
+    print(msg, file=sys.stderr)
+    sys.exit(1)
+
+
+@dataclass
+class RoundCode:
+    year: int
+    cat: str
+    seq: int
+
+    def __str__(self):
+        return f'{self.year}-{self.cat}-{self.seq}'
+
+    @staticmethod
+    def parse(code: str) -> Optional['RoundCode']:
+        m = re.match(r'(\d+)-([A-Z0-9]+)-(\d+)', code)
+        if m:
+            return RoundCode(year=int(m[1]), cat=m[2], seq=int(m[3]))
+        else:
+            return None
+
+
+def get_round_by_code(code: RoundCode) -> Optional[db.Round]:
+    return db.get_session().query(db.Round).filter_by(year=code.year, category=code.cat, seq=code.seq).one_or_none()