Расчет странового коэффициента УЕФА

  • Tutorial
image

В футболе есть два популярных турнира: Лига Чемпионов и Лига Европы. На основании их результатов рассчитывается так называемый Рейтинг футбольных ассоциаций. На базе этого рейтинга в дальнейшем определяется, какое количество команд от каждой страны будет участвовать в последующих турнирах.

В этой статье я создам приложение на основе открытой и бесплатной платформы lsFusion, которое будет рассчитывать этот рейтинг. Оно будет хранить все свои данные в PostgreSQL, предоставлять веб интерфейс по их изменению и отображению с возможностями фильтрации и сортировки, а также импортировать результаты матчей при помощи специального API.

Весь код для реализации этого приложения будет состоять из около 300 значащих строк.

Доменная логика


Создание любой информационной системы начинается с задания доменной логики.
В первую очередь, логично выделить самые простые справочники, у которых есть только код и наименование:

  • Турнир. Лига Чемпионов или Лига Европы.
  • Сезон. 2018-2019 / 2017-2018 и т.д.
  • Раунд. Финал, Полуфинал, Групповая стадия и т.д. Его можно считать как композицию к турниру, но в данной реализации я выделил его как отдельную сущность.
  • Страна. В данном приложении используется как Футбольная ассоциация. Например, клуб Монако находится в стране Монако, но играет во французском чемпионате.
  • Клуб. Барселона, Реал, Манчестер Юнайтед и т.д.

Так как для их объявления в lsFusion используется однотипная логика, то объявим метакод (или шаблон кода), который будет генерировать соответствующую логику:

Метакод по объявлению справочников
META defineMasterObject(object, caption, captions, nameLength)
   CLASS ###object caption;
  
   id 'Код' = DATA INTEGER (###object);
   object (INTEGER id) = GROUP AGGR ###object o BY id(o); // возвращает по коду объект
  
   name 'Имя' = DATA ISTRING (###object) CHARWIDTH nameLength;
   object (ISTRING name) = GROUP AGGR ###object o BY name(o); // возвращает по наименованию объект
  
   FORM object caption
       OBJECTS o = ###object PANEL
       PROPERTIES(o) id, name
      
       EDIT ###object OBJECT o
   ;
  
   FORM object##s captions
       OBJECTS o = ###object
       PROPERTIES(o) READONLY id, name
       PROPERTIES(o) NEWSESSION NEWEDITDELETE
   ;
  
   FORM dialog###object##s captions
       OBJECTS o = ###object
       PROPERTIES(o) READONLY id, name
      
       LIST ###object OBJECT o
   ;
  
   NAVIGATOR {
       NEW object##s;
   }
END


Он будет объявлять:

  • Класс с заданным именем
  • Свойства с кодом и наименованием для нового класса
  • Три формы: редактирование объекта, форму со списком всех объектов, которая затем добавляется в навигатор, диалог по выбору этого объекта. В качестве диалога можно использовать вторую форму, но тогда у пользователя будет возможность изменять объекты при выборе, что может приводить к ошибкам со стороны пользователей.

В метакод передаются четыре параметра:

  • Идентификатор (object). С таким именем будут создаваться классы и формы. Конструкция ### используется, чтобы в результирующий код первая буква идентификатора делалась заглавной.
  • Название в единственном числе. Используется для заголовка класса и формы.
  • Название во множественном числе. Используется для формы со списком и диалога.
  • Длина наименования. В наименованиях разных объектов ожидаются разные длины, что важно при построении интерфейса.

Используя созданный метакод добавим пять вышеописанных сущностей:
@defineMasterObject(tournament, 'Турнир''Турниры'20);
@defineMasterObject(season, 'Сезон''Сезоны'5);
@defineMasterObject(round, 'Раунд''Раунды'15);
@defineMasterObject(country, 'Страна''Страны'10);
@defineMasterObject(team, 'Клуб''Клубы'20);


Сгенерированный код, например, для турнира будет выглядеть следующим образом:
CLASS Tournament 'Турнир';

id 'Код' = DATA INTEGER (Tournament);
tournament (INTEGER id) = GROUP AGGR Tournament o BY id(o);

name 'Имя' = DATA ISTRING (Tournament) CHARWIDTH 20;
tournament (ISTRING name) = GROUP AGGR Tournament o BY name(o);

FORM tournament 'Турнир'
   OBJECTS o = Tournament PANEL
   PROPERTIES(o) id, name
  
   EDIT Tournament OBJECT o
;

FORM tournaments 'Турниры'
   OBJECTS o = Tournament
   PROPERTIES(o) READONLY id, name
   PROPERTIES(o) NEWSESSION NEWEDITDELETE
;

FORM dialogTournaments 'Турниры'
   OBJECTS o = Tournament
   PROPERTIES(o) READONLY id, name
  
   LIST Tournament OBJECT o
;

NAVIGATOR {
   NEW tournaments;
}

К сгенерированной логике клубов добавим ссылку на страну. Для этого сначала создадим соответствующее свойство, которое затем вынесем на формы редактирования и просмотра клуба:
country = DATA Country (Team);
nameCountry 'Страна' (Team t) = name(country(t));

EXTEND FORM team PROPERTIES(o) nameCountry;
EXTEND FORM teams PROPERTIES(o) READONLY nameCountry;

Всю созданную логику положим в отдельный модуль Master (файл Master.lsf).

Теперь создадим сущность Лига. Она будет определять турнир определенного сезона. Например, Лига Чемпионов 2017-18 или Лига Европы 2018-19. У лиги не будет наименования, а только ссылки на турнир и сезон. Поэтому предыдущим метакодом пользоваться не будем, а сделаем аналогичную логику и поместим в новый модуль League:
MODULE League;

REQUIRE Master;

CLASS League 'Лига';

id 'Код' = DATA INTEGER (League);
league (INTEGER id) = GROUP AGGR League o BY id(o);

tournament = DATA Tournament (League);
nameTournament 'Турнир' (League l)= name(tournament(l));

season = DATA Season(League);
nameSeason 'Сезон' (League l)= name(season(l));

FORM league 'Лига'
   OBJECTS o = League PANEL
   PROPERTIES(o) id, nameTournament, nameSeason
  
   EDIT League OBJECT o
;

FORM leagues 'Лиги'
   OBJECTS o = League
   PROPERTIES(o) READONLY id, nameTournament, nameSeason
   PROPERTIES(o) NEWSESSION NEWEDITDELETE
;

FORM dialogLeagues 'Лиги'
   OBJECTS o = League
   PROPERTIES(o) READONLY id, nameTournament, nameSeason
  
   LIST League OBJECT o
;

NAVIGATOR {
   NEW leagues;
}

И, наконец, добавим логику матчей. Для этого создадим класс Матч, который будет ссылаться на лигу и раунд. Для него также будут заданы клубы, которые в нем участвовали, и результат. Все это поместим в отдельный модуль Match:
MODULE Match;

REQUIRE League;

CLASS Match 'Матч';

id 'Код' = DATA INTEGER (Match);
match (INTEGER id) = GROUP AGGR Match o BY id(o);

dateTime 'Время' = DATA DATETIME (Match); 

league = DATA League (Match);

tournament (Match m) = tournament(league(m));
nameTournament 'Турнир' (Match m) = name(tournament(m)); 

season(Match m) = season(league(m));
nameSeason 'Сезон' (Match m) = name(season(m)); 

round = DATA Round (Match);
nameRound 'Раунд' (Match m) = name(round(m));

homeTeam = DATA Team (Match);
nameHomeTeam 'Хозяева' (Match m) = name(homeTeam(m));

awayTeam = DATA Team (Match);
nameAwayTeam 'Гости' (Match m) = name(awayTeam(m));

goalsHome 'Голы (хозяева)' = DATA INTEGER (Match);
goalsAway 'Голы (гости)' = DATA INTEGER (Match);

FORM match 'Матч'
   OBJECTS o = Match PANEL
   PROPERTIES(o) id, dateTime, nameTournament, nameSeason, nameRound,
                 nameHomeTeam, goalsHome, goalsAway, nameAwayTeam
  
   EDIT Match OBJECT o
;

FORM matches 'Матчи'
   OBJECTS o = Match
   PROPERTIES(o) READONLY id, dateTime, nameTournament, nameSeason, nameRound,
                          nameHomeTeam, goalsHome, goalsAway, nameAwayTeam
   PROPERTIES(o) NEWSESSION NEWEDITDELETE

   LIST Match OBJECT o
;

NAVIGATOR {
   NEW matches;
}


Импорт данных


К сожалению, мне удалось найти только один общедоступный и бесплатный API, поддерживающее все еврокубки. Это API Football. Однако, там есть свои проблемы:

  • Отсутствуют результаты до 2016 года.
  • Отсутствуют результаты квалификации Лиги Европы до 2018 года.
  • Есть определенные ошибки в данных. Например, Irtysh Pavlodar отнесен к России, хотя этот клуб представляет Казахстан. Также Europa Fc почему-то относится к Испании вместо Гибралтара.


Ошибки в данных можно исправить вручную при помощи созданных ранее форм. Однако, так как расчет общего коэффициента идет на основе последних пяти лет, то посчитать общий коэффициент из данных API Football, к сожалению, не получится. Если кто-то в комментариях предложит, откуда получить нужные данные в любом формате за предыдущие годы, то буду очень признателен. Но, так как есть полные данные за 2018 год, то можно будет проверить корректность расчета хотя бы за этот год.

Нужный нам API реализован в виде HTTP запросов, где параметры передаются через url, а в заголовке указывается специальный ключ доступа. Объявим соответствующую логику:
host = 'api-football-v1.p.rapidapi.com';
key 'Ключ к API Football' = DATA STRING () CHARWIDTH 50;

url = 'https://' + host() + '/v2';

headers(TEXT name) = CASE
   WHEN name = 'x-rapidapi-host' THEN host()
   WHEN name = 'x-rapidapi-key' THEN key();

Все действия по импорту данных поместим на ранее созданную форму leagues. Туда же поместим ключ доступа в тулбар таблицы со списком лиг:
EXTEND FORM leagues
   PROPERTIES() key DRAW o TOOLBAR
;


Для начала реализуем получение списка лиг. Для этого в API Football есть специальный url: /leagues. GET запрос к нему возвращает JSON вида:

Ответ
{
   "api":{
      "results":2,
      "leagues":[
         {
            "league_id":1,
            "name":"2018 Russia World Cup",
            "country":"World",
            "country_code":null,
            "season":2018,
            "season_start":"2018-06-14",
            "season_end":"2018-07-15",
            "logo":"https://www.api-football.com/public/leagues/1.png",
            "flag":null,
            "standings":0,
            "is_current":1
         },
         {
            "league_id":2,
            "name":"Premier League",
            "country":"England",
            "country_code":"GB",
            "season":2018,
            "season_start":"2018-08-10",
            "season_end":"2019-05-12",
            "logo":"https://www.api-football.com/public/leagues/2.png",
            "flag":"https://www.api-football.com/public/flags/gb.svg",
            "standings":1,
            "is_current":1
         }
      ]
   }
}


Для формирования GET запроса к нему и записи body ответа используется следующая конструкция:
LOCAL result = FILE();
EXTERNAL HTTP GET url() + '/leagues' HEADERS headers TO result;

Она записывает результат в локальное свойство result без параметров типа FILE.

Для разбора файла в формате JSON строится форма, структура которой соответствует структуре JSON. Сгенерировать ее можно в IDE при помощи пункта меню:

image

Для вышеприведенного JSON’а форма будет выглядеть следующим образом (с учетом только тех значений, которые будут импортироваться):
GROUP api;

tournamentName = DATA LOCAL STRING(INTEGER);
seasonName = DATA LOCAL STRING(INTEGER);
leagueId = DATA LOCAL INTEGER(INTEGER);

FORM importLeagues
   OBJECTS leagues = INTEGER IN api
   PROPERTIES(leagues) name = tournamentName, season = seasonName, league_id = leagueId
;

Для непосредственно импорта из свойства result JSON’а в формате формы importLeagues используется следующая команда:
IMPORT importLeagues JSON FROM result();

После ее выполнения в свойства tournamentName, seasonName и leagueId будут помещены соответствующие значения из JSON файла:

image

То есть значение для tournamentName(0) будет “World Cup”, а в tournamentName(1) — “Premier League”.

К сожалению, в API Football вообще нет сущности турнир. Единственным способом связать все лиги является наименование, которое совпадает для лиг одного турнира из разных сезонов. Для этого в импорте сначала группируем все наименования импортированных лиг и, в случае отсутствия в базе, создаем новые турниры:
FOR [GROUP SUM 1 BY tournamentName(INTEGER i)](STRING tn) AND NOT tournament(tn) DO NEW t = Tournament {
   name(t) <- tn;
}

Для сезонов также нет кодов, поэтому при импорте лиг они создаются аналогично. После того, как отсутствующие объекты созданы, импортируются непосредственно лиги. Поиск турниров и сезонов идет по наименованию при помощи построенных ранее через GROUP AGGR свойств:
FOR leagueId(INTEGER i) AND NOT league(leagueId(i)) DO NEW l = League {
   id(l) <- leagueId(i);
   tournament(l) <- tournament(tournamentName(i));
   season(l) <- season(seasonName(i));
}

По умолчанию, данные загрузятся, но в базу будут сохранены только когда пользователь нажмет кнопку Сохранить на форме. При необходимости можно в конце действия добавить команду APPLY, чтобы она сразу сохранилась в базу без предпросмотра.

И, наконец, добавляем действие по импорту на форму со списком лиг:
EXTEND FORM leagues
   PROPERTIES() importLeagues DRAW o TOOLBAR
;

Аналогичным образом реализуем импорт клубов и матчей. Однако, так как API предоставляет возможность импортировать их только для конкретной лиги, то действие должно принимать на вход лигу:

Импорт клубов и матчей
// Импорт клубов
teamId = DATA LOCAL INTEGER(INTEGER);
teamName = DATA LOCAL STRING(INTEGER);
countryName = DATA LOCAL STRING(INTEGER);

FORM importTeams
   OBJECTS teams = INTEGER IN api
   PROPERTIES(teams) team_id = teamId, name = teamName, country = countryName
;

importTeams 'Импортировать клубы' (League l) {
   LOCAL result = FILE();
   EXTERNAL HTTP GET url() + '/teams/league/' + id(l) HEADERS headers TO result;

   IMPORT importTeams JSON FROM result();
   FOR [GROUP SUM 1 BY countryName(INTEGER i)](STRING cn) AND NOT country(cn) DO NEW c = Country {
       name(c) <- cn;
   }

   FOR teamId(INTEGER i) AND NOT team(teamId(i)) DO NEW t = Team {
       id(t) <- teamId(i);
       name(t) <- teamName(i);
       country(t) <- country(countryName(i));
   }
}

// Импорт матчей

matchId = DATA LOCAL INTEGER(INTEGER);
dateTime = DATA LOCAL STRING(INTEGER);
roundName = DATA LOCAL STRING(INTEGER);

GROUP homeTeam;
homeTeamId = DATA LOCAL INTEGER (INTEGER);

GROUP awayTeam;
awayTeamId = DATA LOCAL INTEGER (INTEGER);

goalsHome = DATA LOCAL INTEGER (INTEGER);
goalsAway = DATA LOCAL INTEGER (INTEGER);

FORM importMatches
   OBJECTS fixtures = INTEGER IN api
   PROPERTIES(fixtures) fixture_id = matchId, league_id = leagueId, event_date = dateTime, round = roundName, 
                        homeTeamId IN homeTeam EXTID 'team_id',
                        awayTeamId IN awayTeam EXTID 'team_id',
                        goalsHomeTeam = goalsHome, goalsAwayTeam = goalsAway
;

importMatches 'Импортировать матчи' (League l) {
   LOCAL result = FILE();
   EXTERNAL HTTP GET url() + '/fixtures/league/' + id(l) HEADERS headers TO result;

   IMPORT importMatches JSON FROM result();
   FOR [GROUP SUM 1 BY awayTeamId(INTEGER i)](INTEGER id) AND NOT team(id) DO {
       MESSAGE 'Не найдена домашний клуб с кодом ' + id;
       RETURN;
   }

   FOR [GROUP SUM 1 BY awayTeamId(INTEGER i)](INTEGER id) AND NOT team(id) DO {
       MESSAGE 'Не найдена гостевой клуб с кодом ' + id;
       RETURN;
   }

   FOR [GROUP SUM 1 BY roundName(INTEGER i)](STRING rn) AND NOT round(rn) DO NEW r = Round {
       name(r) <- rn;
   }

   FOR matchId(INTEGER i) AND NOT match(matchId(i)) DO NEW m = Match {
       id(m) <- matchId(i);
       dateTime(m) <- toDateTimeFormat(left(dateTime(i),19), 'yyyy-MM-ddThh24:mi:ss');
       league(m) <- league(leagueId(i));
       round(m) <- round(roundName(i));
       homeTeam(m) <- team(homeTeamId(i));
       awayTeam(m) <- team(awayTeamId(i));
       goalsHome(m) <- goalsHome(i);
       goalsAway(m) <- goalsAway(i);
   }
}

Для матчей есть своя особенность: коды команд идут внутри дополнительных тегов homeTeam и awayTeam. Для них создаются соответствующие группы по аналогии с api. При этом внутри они имеют одинаковые тэги team_id. Так как на форму нельзя добавлять свойства с одинаковым именем, используется специальное ключевое слово EXTID, которое определяет имя тэг в импортируемом JSON.

Для того, чтобы все импорты были на одной форме, и так как они привязаны к лигам, то выносим их всех на одну форму. Кроме того, добавляем на форму команды и матчи, чтобы иметь возможность видеть перед сохранением то, что импортируется:
EXTEND FORM leagues
   OBJECTS t = Team
   PROPERTIES(t) READONLY id, name
  
   PROPERTIES importTeams(o) DRAW t TOOLBAR

   OBJECTS m = Match
   PROPERTIES(m) READONLY id, dateTime, nameRound, nameHomeTeam, goalsHome, goalsAway, nameAwayTeam
   FILTERS league(m) = o
   
   PROPERTIES importMatches(o) DRAW m TOOLBAR
;

DESIGN leagues {
   OBJECTS {
       NEW leagueDetails {
           fill = 2;
           type = SPLITH;
           MOVE BOX(t);
           MOVE BOX(m);
       }
   }
}


Результирующая форма будет выглядеть следующим образом:
image

Весь импорт положим в отдельный модуль APIFootball.

Расчет коэффициента


Перейдем непосредственно к расчету странового коэффициента УЕФА. Весь код логично положить в специально заведенный для этого модуль UEFA.

Для начала учтем, что API Football предоставляет интерфейс по импорту всех матчей, а не только еврокубков. Поэтому отделим именно еврокубковые матчи по наименованию турнира (правильнее завести отдельное первичное свойство для этого, но реализацию свойств всегда можно будет изменить без модификации всей остальной логики):
isCL (Tournament t) = name(t) = 'Champions League';
isEL (Tournament t) = name(t) = 'Europa League';

isUL (Tournament t) = isCL(t) OR isEL(t);
isUL (Match m) = isUL(tournament(m)); 

Для начала рассчитаем очки, которая получает каждый клуб в сезоне за результаты конкретных матчей.
В течение этого периода каждая команда получает:
2 очка в случае победы;
1 очко в случае ничьей.
Начиная с 1999 года, эти очки делятся на два в случае, если они заработаны в квалификационных раундах, то есть:
1 очко в случае победы;
0,5 очка за ничейный результат.


Создадим вспомогательные свойства, которые определяют отношение матча и клуба:
played (Team t, Match m) = homeTeam(m) = t OR awayTeam(m) = t;
won (Team t, Match m) = (homeTeam(m) = t AND goalsHome(m) > goalsAway(m)) OR (awayTeam(m) = t AND goalsHome(m) < goalsAway(m));
draw (Team t, Match m) = played(t, m) AND goalsHome(m) = goalsAway(m);

Для определения того, сколько в каждом матче набирается очков, добавим первичное свойство числового типа для раунда, которое по умолчанию будет равно единице:
dataMatchCoeff = DATA NUMERIC[10,1] (Round);
matchCoeff 'Коэффициент матчей' (Round r) = OVERRIDE dataMatchCoeff(r), 1.0;

Дальше считаем очки за победы и ничьи и складываем вместе:
wonPoints 'Очки за победы' (Season s, Team t) =
   GROUP SUM 2 * matchCoeff(round(Match m)) IF won(t, m) AND season(m) = s AND isUL(m);
drawPoints 'Очки за ничью' (Season s, Team t) =
   GROUP SUM 1 * matchCoeff(round(Match m)) IF draw(t, m) AND season(m) = s AND isUL(m);
matchPoints 'Очки за матчи' (Season s, Team t) = wonPoints(s, t) (+) drawPoints(s, t) MATERIALIZED;

Очки за матчи помечаем как MATERIALIZED, чтобы они сохранялись в таблицу, а не рассчитывались каждый раз.

Теперь нужно еще посчитать бонусные очки:
Кроме этого, начисляются бонусные очки:
По 1 очку даётся в случае выхода команды в четвертьфинал, полуфинал и финал в европейских кубках;
4 очка за выход в групповую стадию Лиги чемпионов (до 1996 года — 2 очка, с 1997 по 2003 — 1 очко, c 2004 по 2008 — 3 очка);
5 очков в случае выхода команды в 1/8 финала Лиги чемпионов (до 2008 года — 1 очко).
В расчёт берутся только сыгранные матчи (технические поражения не учитываются). Матчи, завершившиеся серией послематчевых пенальти, при подсчёте коэффициента считаются в соответствии с тем результатом, который зафиксирован по результатам игры в основное и дополнительное время.

В этой реализации будем считать, что клуб прошел в раунд турнира, если он сыграл в нем хотя бы один матч. Для этого посчитаем, сколько матчей сыграл клуб в конкретном сезоне, турнире, раунде:
played 'Играл' (Season s, Tournament t, Round r, Team tm) =
   GROUP SUM 1 IF played(tm, Match m) AND round(m) = r AND tournament(m) = t AND season(m) = s;


Теперь нужно определить сколько начислять очков за проход в конкретный раунд. Так как это зависит от турнира (например, за проход в ⅛ Лиги Чемпионов дается 5 очков, а в Лиге Европы — ничего). Для этого введем первичной свойство:
bonusPoints 'Бонус за проход' = DATA NUMERIC[10,1] (Tournament, Round);

Теперь посчитаем бонусные очки и суммарное количество очков по клубу за сезон:
bonusPoints 'Бонусные очки' (Season s, Team tm) = GROUP SUM bonusPoints(Tournament t, Round r) IF played(s, t, r, tm) MATERIALIZED;

points 'Очки' (Season s, Team tm) = matchPoints(s, tm) (+) bonusPoints(s, tm);

Наконец, переходим непосредственно к страновому коэффициенту.
Для расчёта рейтинга ассоциации все очки, набранные клубами, принявшими участие в Лиге чемпионов и Лиге Европы, складываются, и результат делится на количество клубов от этой ассоциации[2][3].

Посчитаем количество клубов по каждой ассоциации, которые приняли участие в еврокубках:
matchesUL 'Матчей в еврокубках' (Season s, Team t) = GROUP SUM 1 IF played(t, Match m) AND season(m) = s AND isUL(m);
teams 'Клубов' (Season s, Country c) = GROUP SUM 1 IF matchesUL(s, Team t) AND country(t) = c;

Теперь считаем общее количество очков по ассоциации за сезон и делим на количество клубов:
totalPoints 'Очки (всего)' (Season s, Country c) = GROUP SUM points(s, Team t) IF country(t) = c;
points 'Очки' (Season s, Country c) = trunc(NUMERIC[13,4](totalPoints(s, c)) / teams(s, c), 3);


Рейтинг страны представляет собой сумму коэффициентов страны за предыдущие 5 лет.


Для этого проводим нумерацию всех сезонов начиная с последнего по внутреннему коду (будем считать, что последние добавлялись позже и имеют больший код).:
index 'Индекс' (Season s) = PARTITION SUM 1 IF s IS Season ORDER DESC s;

При необходимости, можно ввести отдельное поле или нумеровать по наименованию.
Осталось только рассчитать итоговый рейтинг по стране:
rating 'Рейтинг' (Country c) = GROUP SUM points(Season s, c) IF index(s) <= 5;

Выше мы объявили коэффициенты для турниров и раундов. Добавим их на форму редактирования турнира, при этом фильтруя только те раунды, которые были в этих турнирах:
matches (Tournament t, Round r) = GROUP SUM 1 IF tournament(Match m) = t AND round(m) = r;

EXTEND FORM tournament
   OBJECTS r = Round
   PROPERTIES name(r) READONLY, matchCoeff(r), bonusPoints(o, r)
   FILTERS matches(o, r)
;


Настройки коэффициентов, например, для Лиги Чемпионов нужно установить вот так:
image

Нарисуем форму, которая будет отображать рейтинг, где для каждой страны будут показываться команды, а для каждой команды ее матчи:
FORM countryCoefficientUEFA 'Коэффициент стран UEFA'
   OBJECTS s = Season
   FILTERS index(s) <= 5
  
   OBJECTS c = Country
   PROPERTIES(c) READONLY name, rating
   PROPERTIES(s, c) COLUMNS (s) points HEADER 'Очки : ' + name(s), teams HEADER 'Клубы : ' + name(s)
  
   OBJECTS t = Team
   PROPERTIES(t) READONLY nameCountry, name
   PROPERTIES(s,t) COLUMNS (s) HEADER name(s) points BACKGROUND matchesUL(s, t)
   FILTERGROUP country
       FILTER 'По стране' country(t) = c DEFAULT
      
   OBJECTS m = Match
   PROPERTIES(m) READONLY dateTime, nameTournament, nameSeason, nameRound,
                          nameHomeTeam,
                          goalsHome BACKGROUND goalsHome(m) > goalsAway(m),
                          goalsAway BACKGROUND goalsHome(m) < goalsAway(m),
                          nameAwayTeam
   FILTERS played(t, m)
   ORDER dateTime(m) DESC
;

DESIGN countryCoefficientUEFA {
   OBJECTS {
       NEW countryDetails {
           type = SPLITH;
           fill = 0.5;
           MOVE BOX(t);
           MOVE BOX(m);
       }
   }

}

NAVIGATOR {
   NEW countryCoefficientUEFA;
}

Выглядеть результирующая форма будет вот так:
image
Цветом в таблицах клубов показывается, когда он принимал участие в сезонах, а в таблице матчей — кто победил.
На картинке видно, что рейтинги за 2018 год подсчитаны точно так же, как в википедии. За предыдущие года, как говорилось выше, API Football предоставляет не всю информацию.

Итог



Мы построили небольшое приложение, которое полностью описывается вышеописанным кодом и хранит свои данные в PostgreSQL, предоставляет веб интерфейс по просмотру и редактированию данных. При этом оно будет эффективно работать на больших объемах, так как все формы считывают только видимое окно. Также из коробки работают фильтры, сортировки, выгрузки в Excel и прочее.

Следует отметить, как легко при помощи платформы задача по расчету коэффициента была декомпозирована на отдельные свойства. При выполнении вся эта логика будет транслирована в SQL запросы, и все расчеты будут произведены непосредственно на сервере базы данных с использованием всех оптимизаций СУБД.

Пример работы приложения с загруженными в него данными можно посмотреть по адресу: https://demo.lsfusion.org/euroleague. Логин guest без пароля. Пользователю включен режим readonly.

Желающие могут установить себе все локально и, например, моделировать коэффициенты путем ввода результатов будущих матчей. Все описанные выше модули приложения размещены на github. После автоматической установки нужно будет просто подкинуть эти файлы в соответствующую папку из инструкции и перезапустить сервер.

Для того, чтобы загрузить данные из API Football, нужно зарегистрироваться у них и получить ключ API. Требует карточку, но если делать не более 50 запросов в день, то списываться с нее ничего не будет.

Кроме того, можно запустить это приложение онлайн в соответствующем разделе на сайте. На вкладке Платформа нужно выбрать пример Расчет коэффициентов УЕФА и нажать Play.

Кстати, если кому-то необходимо реализовать какую-то несложную систему, для которой Excel уже не подходит, то пишите в комментариях. В целях обучения возможностям платформы постараемся ее реализовать и напишем соответствующую статью.
  • +16
  • 1,9k
  • 5
lsFusion
67,23
Не очередной язык программирования
Поделиться публикацией

Комментарии 5

    0
    Учёт движения ключей автомобилей в автосалоне трейд ин — достаточно простая задача? Данные о наличии/отсутствии ключей получаются с HTTP сервиса управления ключницами.
      0
      Да, задача несложная. Но тут нужно хотя бы краткое описание задачи. Какие операции и процессы используются. Какую информацию нужно получать. Если нужен обмен с HTTP сервисом, то хотя бы базовое описание API. Если все это будет, то можем сделать.
        0
        Ок. Причешу ТЗ сегодня-завтра, и отправлю.
      0
      Иногда при знакомстве с очередной записью труженика из команды lsFusion хочется просто отметить какой-то положительный момент, сиюминутное впечатление от публикации. В данном случае я ставлю целью отметить последовательное изложение материала и его качественное оформление.

      Бывает, хабровец, уходя с головой в описываемые технологии, напрочь забывает, что не все читатели плавают в мире, из которого он всплыл, чтобы поделиться с окружающими своей нирваной.

      Но KabakovichMaxim подает информацию очень аккуратно, добавляя матчасть из мира спорта (я, например, полный профан во всех этих *ИФА'х) порциями, по мере необходимости.

      Подозреваю, что эта аккуратность отчасти вызвана стремлением преподнести свою технологию как нечто максимально логичное, простое и прозрачное. Ну что ж, ничего плохого в этом стремлении нет.
        0
        Спасибо за отзыв.

        В этом преимущество любой декларативной технологии: она близка к текстовому описанию. Можно брать каждое утверждение в задаче и записывать его соответствующей командой по очередь. Здесь старался так и делать.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое