C++23 - Vista chunk_by

Introducción

El futuro estándar del lenguaje C++23, actualmente en estado feature freeze, proporciona nuevas y útiles vistas para su biblioteca de rangos, entre ellas [1]:

  • std::views::adjacentstd::views::adjacent_transform
  • std::views::cartesian_product
  • std::views::chunkstd::views::chunk_by
  • std::views::join_with
  • std::views::repeat
  • std::views::slide
  • std::views::stride
  • std::views::zip, std::views::zip_transform

La vista std::views::chunk_by, centro de atención de este post, agrupa elementos contiguos de un rango a través de un predicado binario.  Así, dado un rango de partida R=[T] de elementos de tipo T y un predicado binario P, la vista devolverá un rango de rangos [[T]] donde cada subrango contiene elementos contiguos de R que cumplen la siguiente condición: para cada elemento ei en el subrango a excepción del primero, la evaluación del predicado para el elemento inmediatamente anterior ei-1 y ei es verdadera, i.e., P(ei-1,ei)=true [2, 3]. Es importante señalar que no se requiere que dicho predicado constituya una relación de equivalencia.

A modo de ejemplo, el rango R=[1,2,3,1,5,4] sería agrupado bajo el predicado menor-estricto-que en la forma [[1,2,3],[1,5],[4]]. Ello contrasta con la vista ranges::views::group_by --ahora deprecada-- de la biblioteca Range-v3 [4], que compara el primer elemento de cada subrango con los siguientes, dando lugar al rango de salida [[1,2,3],[1,5,4]] (véanse más detalles en este post anterior).


Código de ejemplo

A modo de ejercicio práctico que involucre el uso de la vista chunk_by, consideremos un fichero JSON Lines roland_garros.jsonl que contenga el palmarés histórico del individual masculino de Roland Garros - French Open en el intervalo 1925-2022. Cada línea recoge el nombre del campeón ("name"), nacionalidad ("country") y año del campeonato ("year"). Asumiremos que las líneas se encuentran registradas cronológicamente en el fichero:

      {"country":"fra","name":"René Lacoste","year":1925}
      {"country":"fra","name":"Henri Cochet","year":1926}
           ...
      {"country":"ser","name":"Novak Djokovic","year":2021}
      {"country":"esp","name":"Rafael Nadal","year":2022}

El lector dispone del código generador del fichero completo al final del post.

Nos proponemos mostrar por la terminal un listado de récords conseguidos en el campeonato. Nuestro programa debería ser capaz de identificar los períodos en los que un mismo tenista haya ganado el palmarés de forma consecutiva, con el fin de producir el resultado mostrado en la siguiente captura:

Así, por ejemplo, Rafael Nadal acumuló cinco certámenes consecutivos en el período 2010-2014, y cuatro victorias consecutivas en los períodos 2005-2008 y 2017-2020. Cuenta también con una victoria adicional en 2022, si bien ésta no aparece señalada en la figura.

Para alcanzar el resultado anterior, nos serviremos del compilador GCC 12, así como de la implementación de la vista chunk_by disponible en la biblioteca TartanLlama/tl desarrollada por Sy Brand (Microsoft) [5]. Para la instalación de la biblioteca, consúltese este post. Esta vista se incluye también en la biblioteca Range-v3 (Release 19).

En primer lugar, incluiremos las cabeceras estándar y no-estándar relevantes para nuestro ejercicio:

   #include <algorithm>    #include <fstream>    #include <functional>    #include <map>    #include <ranges>    #include <string>    #include <string_view>    #include <vector>    #include <boost/hof/proj.hpp>    #include <fmt/core.h>    #include <nlohmann/json.hpp>    #include <tl/chunk_by.hpp>    #include <tl/getlines.hpp>    #include <tl/to.hpp>

Recordemos en este punto que el estándar C++23 permitirá la importación completa de la biblioteca estándar del lenguaje --con una mejora sustancial del tiempo de compilación-- mediante la cláusula sencilla:

   import std;

A continuación, definamos un agregado de datos Champion para deserializar y almacenar la información de cada línea JSON. Hacemos uso aquí de la biblioteca nlohmann/json [6]:

   struct Champion {       std::string name,                   country;       int year;    };    NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Champion, name, country, year)

Deserializamos entonces el fichero, generando un vector estándar de objetos de tipo Champion (véase este post para más detalles acerca de la técnica IILE):

   auto main() -> int    try {       namespace bhof = boost::hof;       namespace stdr = std::ranges;       namespace stdv = std::views;       namespace tlv = tl::views;       auto champions = /* IILE */ [] -> std::vector<Champion> {          auto ifs = std::ifstream{"../../roland_garros.jsonl", std::ios::binary};          if (!ifs) throw std::ios_base::failure{"unable to open the JSONL file"};          return tlv::getlines(ifs)               | stdv::transform([](std::string_view ln){                    return nlohmann::json::parse(ln).get<Champion>();                 })               | tl::to<std::vector>();       }();

A continuación, crearemos un mapa estándar cuyas claves k, ordenadas de mayor a menor, se corresponderán con los números de certámenes consecutivos logrados por algún tenista en un período histórico determinado (hasta la fecha de publicación de este post, se han registrado 1, 2, 3, 4 o 5 años consecutivos de victorias). Para cada clave, el valor asociado será un nuevo mapa que asocie el nombre y país de procedencia de cada tenista con un listado de los años de inicio de sus períodos de k victorias consecutivas:

      auto records = std::map<int, std::map<std::string, std::vector<int>>, stdr::greater>{};       stdr::stable_sort(champions, {}, &Champion::name); // asumido orden cronológico en JSONL             for (auto&& name_chunk : champions                              | tlv::chunk_by(bhof::proj(&Champion::name, std::equal_to{}))) {          for (             auto consecutive = [](int i, int j){ return j == i + 1; };             auto&& period_chunk : name_chunk                                 | tlv::chunk_by(bhof::proj(&Champion::year, consecutive))                                 | stdv::filter([](auto&& chunk){ return stdr::distance(chunk) > 1; })          ) {             auto const it = period_chunk.begin();             auto const player_info = fmt::format("{} [{}]", it->name, it->country);             records[stdr::distance(period_chunk)][player_info].push_back(it->year);          }       }

El primer bucle for se encarga de iterar los subrangos del vector characters correspondientes a un mismo tenista. Recordemos que sus entradas se ecuentran ordenadas cronológicamente en base a la propia estructura del archivo JSONL. Por su parte, el bucle for anidado itera los distintos períodos de victorias consecutivas de dicho tenista con el fin de registrar la información relevante (nombre, país y años de inicio de los períodos de victorias consecutivas) en el mapa. En nuestro análisis, la vista filter nos permite tomar en consideración únicamente períodos de dos o más años de duración. Véase este post para más información acerca de las proyecciones con Boost.HOF.

Hagamos notar que, si bien C++ carece de una sintaxis concisa para expresiones lambda, ésta puede lograrse en casos sencillos mediante bibliotecas como Boost.Lambda2 [7]. Así, asumida la inclusión de la cabecera <boost/lambda2.hpp> y el uso de la cláusula using namespace boost::lambda2, los bucles anteriores podrían rescribirse de forma conveniente como:

      for (auto&& name_chunk : champions                              | tlv::chunk_by(bhof::proj(&Champion::name, _1 == _2))) {          for (auto&& period_chunk : name_chunk                                 | tlv::chunk_by(bhof::proj(&Champion::year, _2 == _1 + 1))                                 | stdv::filter(std::bind(stdr::distance, _1) > 1)) {             auto const it = period_chunk.begin();             auto const player_info = fmt::format("{} [{}]", it->name, it->country);             records[stdr::distance(period_chunk)][player_info].push_back(it->year);          }       }

Tan sólo resta mostrar el resultado del análisis --coincidente con la captura proporcionada más arriba-- a través de la terminal:

      for (auto&& [freq, players] : records) {          fmt::print("{:_^50}\n", fmt::format("{} years in a row", freq));          for (auto&& [player_info, years] : players) {             fmt::print("{:>25}: ", player_info);             for (int const year : years) {                fmt::print("{}-{} | ", year, year + freq - 1);             }             fmt::print("\n");          }       }    }    catch (std::exception const& e) {       fmt::print(stderr, "{}\n", e.what());    }

Código generador del fichero JSONL y fichero de construcción CMakeLists.txt

Se proporciona a continuación el código generador del fichero roland_garros.jsonl [8]. Dicho fichero JSONL debiera ubicarse junto al código fuente main.cpp --analizado en la sección anterior-- y el fichero de construcción CMakeLists.txt facilitado más abajo:

   auto ofs = std::ofstream{"../../roland_garros.jsonl", std::ios::binary};    if (!ofs) throw std::ios::failure{"unable to open the file to save data"};        ofs << R"({"year": 1925, "country": "fra", "name": "René Lacoste"})"_json << '\n'        << R"({"year": 1926, "country": "fra", "name": "Henri Cochet"})"_json << '\n'       << R"({"year": 1927, "country": "fra", "name": "René Lacoste"})"_json << '\n'       << R"({"year": 1928, "country": "fra", "name": "Henri Cochet"})"_json << '\n'       << R"({"year": 1929, "country": "fra", "name": "René Lacoste"})"_json << '\n'       << R"({"year": 1930, "country": "fra", "name": "Henri Cochet"})"_json << '\n'       << R"({"year": 1931, "country": "fra", "name": "Jean Borotra"})"_json << '\n'       << R"({"year": 1932, "country": "fra", "name": "Henri Cochet"})"_json << '\n'       << R"({"year": 1933, "country": "aus", "name": "Jack Crawford"})"_json << '\n'       << R"({"year": 1934, "country": "ger", "name": "Gottfried von Cramm"})"_json << '\n'       << R"({"year": 1935, "country": "grb", "name": "Fred Perry"})"_json << '\n'       << R"({"year": 1936, "country": "ger", "name": "Gottfried von Cramm"})"_json << '\n'       << R"({"year": 1937, "country": "ger", "name": "Henner Henkel"})"_json << '\n'       << R"({"year": 1938, "country": "usa", "name": "Don Budge"})"_json << '\n'       << R"({"year": 1939, "country": "usa", "name": "Don McNeill"})"_json << '\n'       << R"({"year": 1946, "country": "fra", "name": "Marcel Bernard"})"_json << '\n'       << R"({"year": 1947, "country": "hun", "name": "Jozsef Asboth"})"_json << '\n'       << R"({"year": 1948, "country": "usa", "name": "Frank Parker"})"_json << '\n'       << R"({"year": 1949, "country": "usa", "name": "Frank Parker"})"_json << '\n'       << R"({"year": 1950, "country": "usa", "name": "Budge Patty"})"_json << '\n'       << R"({"year": 1951, "country": "egy", "name": "Jaroslav Drobny"})"_json << '\n'       << R"({"year": 1952, "country": "egy", "name": "Jaroslav Drobny"})"_json << '\n'       << R"({"year": 1953, "country": "aus", "name": "Ken Rosewall"})"_json << '\n'       << R"({"year": 1954, "country": "usa", "name": "Tony Trabert"})"_json << '\n'       << R"({"year": 1955, "country": "usa", "name": "Tony Trabert"})"_json << '\n'       << R"({"year": 1956, "country": "aus", "name": "Lew Hoad"})"_json << '\n'       << R"({"year": 1957, "country": "swe", "name": "Sven Davidson"})"_json << '\n'       << R"({"year": 1958, "country": "aus", "name": "Mervyn Rose"})"_json << '\n'       << R"({"year": 1959, "country": "ita", "name": "Nicola Pietrangeli"})"_json << '\n'       << R"({"year": 1960, "country": "ita", "name": "Nicola Pietrangeli"})"_json << '\n'       << R"({"year": 1961, "country": "esp", "name": "Manuel Santana"})"_json << '\n'       << R"({"year": 1962, "country": "aus", "name": "Rod Laver"})"_json << '\n'       << R"({"year": 1963, "country": "aus", "name": "Roy Emerson"})"_json << '\n'       << R"({"year": 1964, "country": "esp", "name": "Manuel Santana"})"_json << '\n'       << R"({"year": 1965, "country": "aus", "name": "Fred Stolle"})"_json << '\n'       << R"({"year": 1966, "country": "aus", "name": "Tony Roche"})"_json << '\n'       << R"({"year": 1967, "country": "aus", "name": "Roy Emerson"})"_json << '\n'       << R"({"year": 1968, "country": "aus", "name": "Ken Rosewall"})"_json << '\n'       << R"({"year": 1969, "country": "aus", "name": "Rod Laver"})"_json << '\n'       << R"({"year": 1970, "country": "tch", "name": "Jan Kodes"})"_json << '\n'       << R"({"year": 1971, "country": "tch", "name": "Jan Kodes"})"_json << '\n'       << R"({"year": 1972, "country": "esp", "name": "Andres Gimeno"})"_json << '\n'       << R"({"year": 1973, "country": "rou", "name": "Ilie Năstase"})"_json << '\n'       << R"({"year": 1974, "country": "swe", "name": "Bjorn Borg"})"_json << '\n'       << R"({"year": 1975, "country": "swe", "name": "Bjorn Borg"})"_json << '\n'       << R"({"year": 1976, "country": "ita", "name": "Adriano Panatta"})"_json << '\n'       << R"({"year": 1977, "country": "arg", "name": "Guillermo Vilas"})"_json << '\n'       << R"({"year": 1978, "country": "swe", "name": "Bjorn Borg"})"_json << '\n'       << R"({"year": 1979, "country": "swe", "name": "Bjorn Borg"})"_json << '\n'       << R"({"year": 1980, "country": "swe", "name": "Bjorn Borg"})"_json << '\n'       << R"({"year": 1981, "country": "swe", "name": "Bjorn Borg"})"_json << '\n'       << R"({"year": 1982, "country": "swe", "name": "Mats Wilander"})"_json << '\n'       << R"({"year": 1983, "country": "fra", "name": "Yannick Noah"})"_json << '\n'       << R"({"year": 1984, "country": "tch", "name": "Ivan Lendl"})"_json << '\n'       << R"({"year": 1985, "country": "swe", "name": "Mats Wilander"})"_json << '\n'       << R"({"year": 1986, "country": "tch", "name": "Ivan Lendl"})"_json << '\n'       << R"({"year": 1987, "country": "tch", "name": "Ivan Lendl"})"_json << '\n'       << R"({"year": 1988, "country": "swe", "name": "Mats Wilander"})"_json << '\n'       << R"({"year": 1989, "country": "usa", "name": "Michael Chang"})"_json << '\n'       << R"({"year": 1990, "country": "ecu", "name": "Andres Gomez"})"_json << '\n'       << R"({"year": 1991, "country": "usa", "name": "Jim Courier"})"_json << '\n'       << R"({"year": 1992, "country": "usa", "name": "Jim Courier"})"_json << '\n'       << R"({"year": 1993, "country": "esp", "name": "Sergi Bruguera"})"_json << '\n'       << R"({"year": 1994, "country": "esp", "name": "Sergi Bruguera"})"_json << '\n'       << R"({"year": 1995, "country": "aut", "name": "Thomas Muster"})"_json << '\n'       << R"({"year": 1996, "country": "rus", "name": "Yevgueni Kafelnikov"})"_json << '\n'       << R"({"year": 1997, "country": "bra", "name": "Gustavo Kuerten"})"_json << '\n'       << R"({"year": 1998, "country": "esp", "name": "Carlos Moya"})"_json << '\n'       << R"({"year": 1999, "country": "usa", "name": "Andre Agassi"})"_json << '\n'       << R"({"year": 2000, "country": "bra", "name": "Gustavo Kuerten"})"_json << '\n'       << R"({"year": 2001, "country": "bra", "name": "Gustavo Kuerten"})"_json << '\n'       << R"({"year": 2002, "country": "esp", "name": "Albert Costa"})"_json << '\n'       << R"({"year": 2003, "country": "esp", "name": "Juan Carlos Ferrero"})"_json << '\n'       << R"({"year": 2004, "country": "arg", "name": "Gaston Gaudio"})"_json << '\n'       << R"({"year": 2005, "country": "esp", "name": "Rafael Nadal"})"_json << '\n'       << R"({"year": 2006, "country": "esp", "name": "Rafael Nadal"})"_json << '\n'       << R"({"year": 2007, "country": "esp", "name": "Rafael Nadal"})"_json << '\n'       << R"({"year": 2008, "country": "esp", "name": "Rafael Nadal"})"_json << '\n'       << R"({"year": 2009, "country": "sui", "name": "Roger Federer"})"_json << '\n'       << R"({"year": 2010, "country": "esp", "name": "Rafael Nadal"})"_json << '\n'       << R"({"year": 2011, "country": "esp", "name": "Rafael Nadal"})"_json << '\n'       << R"({"year": 2012, "country": "esp", "name": "Rafael Nadal"})"_json << '\n'       << R"({"year": 2013, "country": "esp", "name": "Rafael Nadal"})"_json << '\n'       << R"({"year": 2014, "country": "esp", "name": "Rafael Nadal"})"_json << '\n'       << R"({"year": 2015, "country": "sui", "name": "Stanislas Wawrinka"})"_json << '\n'       << R"({"year": 2016, "country": "ser", "name": "Novak Djokovic"})"_json << '\n'       << R"({"year": 2017, "country": "esp", "name": "Rafael Nadal"})"_json << '\n'       << R"({"year": 2018, "country": "esp", "name": "Rafael Nadal"})"_json << '\n'       << R"({"year": 2019, "country": "esp", "name": "Rafael Nadal"})"_json << '\n'       << R"({"year": 2020, "country": "esp", "name": "Rafael Nadal"})"_json << '\n'       << R"({"year": 2021, "country": "ser", "name": "Novak Djokovic"})"_json << '\n'       << R"({"year": 2022, "country": "esp", "name": "Rafael Nadal"})"_json << '\n';

Fichero de construcción CMakeLists.txt:

   cmake_minimum_required(VERSION 3.22)    project(roland_garros       VERSION 0.1.0       DESCRIPTION "Proyecto sencillo de C++ con un solo target"       LANGUAGES CXX    )    add_executable(${PROJECT_NAME} main.cpp)    target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_23)    set_target_properties(${PROJECT_NAME} PROPERTIES       CXX_EXTENSIONS OFF       RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/debug       RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/release    )    #-------------------------------------------------------------------    # integración de bibliotecas de terceros:    find_package(Boost REQUIRED)    find_package(fmt REQUIRED)    find_package(nlohmann_json REQUIRED)    find_package(tl-ranges REQUIRED)    target_link_libraries(${PROJECT_NAME} PRIVATE       Boost::headers         fmt::fmt       nlohmann_json       tl::ranges    )    #-------------------------------------------------------------------    # política de avisos:    if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC")       target_compile_options(${PROJECT_NAME} PRIVATE /W3 /WX)    elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")       target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -pedantic -Werror)    endif()    #-------------------------------------------------------------------    # mensajes a emitir en construcción:    message("-- CMake Generator: ${CMAKE_GENERATOR}")    get_target_property(CFEATURES ${PROJECT_NAME} COMPILE_FEATURES)    message("-- Target compile features: ${CFEATURES}")    get_target_property(COPTIONS ${PROJECT_NAME} COMPILE_OPTIONS)    message("-- Target compile options: ${COPTIONS}")

Referencias bibliográficas:
  1. cppreference.com - C++23 - https://en.cppreference.com/w/cpp/23
  2. P2214R1 - A Plan for C++23 Ranges - https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2214r1.html#the-group_by-family
  3. Ranges-v3 User Manual - https://ericniebler.github.io/range-v3/
  4. ericniebler/range-v3 - https://github.com/ericniebler/range-v3
  5. TartanLlama/tl - https://github.com/TartanLlama/tl
  6. nlohmann/json - https://github.com/nlohmann/json
  7. Boost.Lambda2 - https://www.boost.org/doc/libs/master/libs/lambda2/doc/html/lambda2.html
  8. https://es.wikipedia.org/wiki/Anexo:Campeones_de_Roland_Garros_(individual_masculino)

No hay comentarios:

Publicar un comentario