Search code examples
boostini

Deal with duplicate section names in INI files


I need to load these values from INI file and print them in the application using C++ Boost Library. The sections have duplicate names. I have been restricted to using C++ Boost Library only.

numColors = 4
boardSize = 11
numSnails = 2
[initialization]
id = 0
row = 3
col = 4
orientation = 0
[initialization]
id = 1
row = 5
col = 0
orientation = 1
[color]
id = 0
nextColor = 1
deltaOrientation = +2
[color]
id = 1   
nextColor = 2
deltaOrientation = +1
[color]
id = 2
nextColor = 3
deltaOrientation = -2
[color]
id = 3
nextColor = 0
deltaOrientation = -1

Solution

  • What It Isn't

    In short, this is not INI format at all. It just very loosely resembles it. Which is nice.

    What Is It Instead?

    You don't specify a lot, so I'm going to make assumptions.

    I'm going to, for simplicity, assume that

    • initialization sections precede color sections
    • keys in like sections have the same order always
    • all keys shown are mandatory in like sections
    • the deltas are signed integral values (positive sign being optional)
    • all other values are non-negative integral numbers
    • whitespace is not significant
    • case is significant
    • all numbers are in decimal form (regardless of leading zeros)

    Non-essential deductions (could be used to add more validation):

    • the number of of initializations = numSnails
    • the board size dictates row and col are in [0, boardSize)

    Data Structures

    To represent the file, I'd make:

    namespace Ast {
        struct Initialization {
            unsigned id, row, col, orientation;
        };
    
        struct Color {
            unsigned id, nextColor;
            int deltaOrientation;
        };
    
        struct File {
            unsigned numColors, boardSize, numSnails;
    
            std::vector<Initialization> initializations;
            std::vector<Color>          colors;
        };
    }
    

    That's the simplest I can think of.

    Parsing It

    Is a nice job for Boost Spirit. If we adapt the data structures as Fusion Sequences:

    BOOST_FUSION_ADAPT_STRUCT(Ast::Initialization, id, row, col, orientation)
    BOOST_FUSION_ADAPT_STRUCT(Ast::Color, id, nextColor, deltaOrientation)
    BOOST_FUSION_ADAPT_STRUCT(Ast::File, numColors, boardSize, numSnails,
                              initializations, colors)
    

    We can basically let the parser "write itself":

    template <typename It>
    struct GameParser : qi::grammar<It, Ast::File()> {
        GameParser() : GameParser::base_type(start) {
            using namespace qi;
            start = skip(blank)[file];
    
            auto section = [](std::string name) {
                return copy('[' >> lexeme[lit(name)] >> ']' >> (+eol | eoi));
            };
            auto required = [](std::string name) {
                return copy(lexeme[eps > lit(name)] > '=' > auto_ >
                            (+eol | eoi));
            };
    
            file =
                required("numColors") >
                required("boardSize") >
                required("numSnails") >
                *initialization >
                *color >
                eoi; // must reach end of input
    
            initialization = section("initialization") >
                required("id") >
                required("row") >
                required("col") >
                required("orientation");
                
            color = section("color") >
                required("id") >
                required("nextColor") >
                required("deltaOrientation");
    
            BOOST_SPIRIT_DEBUG_NODES((file)(initialization)(color))
        }
    
      private:
        using Skipper = qi::blank_type;
        qi::rule<It, Ast::File()>                    start;
        qi::rule<It, Ast::File(), Skipper>           file;
        qi::rule<It, Ast::Initialization(), Skipper> initialization;
        qi::rule<It, Ast::Color(), Skipper>          color;
    };
    

    Because of the many assumptions we've made we littered the place with expectation points (operator> sequences, instead of operator>>). This means we get "helpful" error messages on invalid input, like

    Expected: nextColor
    Expected: =
    Expected: <eoi>
    

    See also BONUS section below that improves this a lot

    Testing/Live Demo

    Testing it, we will read the file first and then parse it using that parser:

    std::string read_file(std::string name) {
        std::ifstream ifs(name);
        return std::string(std::istreambuf_iterator<char>(ifs), {});
    }
    
    static Ast::File parse_game(std::string_view input) {
        using SVI = std::string_view::const_iterator;
        static const GameParser<SVI> parser{};
    
        try {
            Ast::File parsed;
            if (qi::parse(input.begin(), input.end(), parser, parsed)) {
                return parsed;
            }
            throw std::runtime_error("Unable to parse game");
        } catch (qi::expectation_failure<SVI> const& ef) {
            std::ostringstream oss;
            oss << "Expected: " << ef.what_;
            throw std::runtime_error(oss.str());
        }
    }
    

    A lot could be improved, but for now it works and parses your input:

    Live On Coliru

    int main() {
        std::string game_save = read_file("input.txt");
    
        Ast::File data = parse_game(game_save);
    }
    

    The absense of output means success.

    BONUS

    Some improvements, instead of using auto_ to generate the right parser for the type, we can make that explicit:

    namespace Ast {
        using Id          = unsigned;
        using Size        = uint8_t;
        using Coord       = Size;
        using ColorNumber = Size;
        using Orientation = Size;
        using Delta       = signed;
    
        struct Initialization {
            Id          id;
            Coord       row;
            Coord       col;
            Orientation orientation;
        };
    
        struct Color {
            Id          id;
            ColorNumber nextColor;
            Delta       deltaOrientation;
        };
    
        struct File {
            Size numColors{}, boardSize{}, numSnails{};
    
            std::vector<Initialization> initializations;
            std::vector<Color>          colors;
        };
    }  // namespace Ast
    

    And then in the parser define the analogous:

    qi::uint_parser<Ast::Id>          _id;
    qi::uint_parser<Ast::Size>        _size;
    qi::uint_parser<Ast::Coord>       _coord;
    qi::uint_parser<Ast::ColorNumber> _colorNumber;
    qi::uint_parser<Ast::Orientation> _orientation;
    qi::int_parser<Ast::Delta>        _delta;
    

    Which we then use e.g.:

    initialization = section("initialization") >
        required("id", _id) >
        required("row", _coord) >
        required("col", _coord) >
        required("orientation", _orientation);
    

    Now we can improve the error messages to be e.g.:

    input.txt:2:13 Expected: <unsigned-integer>
     note: boardSize = (11)
     note:             ^--- here
    

    Or

    input.txt:16:19 Expected: <alternative><eol><eoi>
     note:     nextColor = 1 deltaOrientation = +2
     note:                   ^--- here
    

    Full Code, Live On Coliru

    //#define BOOST_SPIRIT_DEBUG
    #include <boost/spirit/home/qi.hpp>
    #include <fstream>
    #include <sstream>
    #include <iomanip>
    namespace qi = boost::spirit::qi;
    
    namespace Ast {
        using Id          = unsigned;
        using Size        = uint8_t;
        using Coord       = Size;
        using ColorNumber = Size;
        using Orientation = Size;
        using Delta       = signed;
    
        struct Initialization {
            Id          id;
            Coord       row;
            Coord       col;
            Orientation orientation;
        };
    
        struct Color {
            Id          id;
            ColorNumber nextColor;
            Delta       deltaOrientation;
        };
    
        struct File {
            Size numColors{}, boardSize{}, numSnails{};
    
            std::vector<Initialization> initializations;
            std::vector<Color>          colors;
        };
    }  // namespace Ast
    
    BOOST_FUSION_ADAPT_STRUCT(Ast::Initialization, id, row, col, orientation)
    BOOST_FUSION_ADAPT_STRUCT(Ast::Color, id, nextColor, deltaOrientation)
    BOOST_FUSION_ADAPT_STRUCT(Ast::File, numColors, boardSize, numSnails,
                              initializations, colors)
    
    template <typename It>
    struct GameParser : qi::grammar<It, Ast::File()> {
        GameParser() : GameParser::base_type(start) {
            using namespace qi;
            start = skip(blank)[file];
    
            auto section = [](const std::string& name) {
                return copy('[' >> lexeme[lit(name)] >> ']' >> (+eol | eoi));
            };
            auto required = [](const std::string& name, auto value) {
                return copy(lexeme[eps > lit(name)] > '=' > value >
                            (+eol | eoi));
            };
    
            file =
                required("numColors", _size) >
                required("boardSize", _size) >
                required("numSnails", _size) >
                *initialization >
                *color >
                eoi; // must reach end of input
    
            initialization = section("initialization") >
                required("id", _id) >
                required("row", _coord) >
                required("col", _coord) >
                required("orientation", _orientation);
                
            color = section("color") >
                required("id", _id) >
                required("nextColor", _colorNumber) >
                required("deltaOrientation", _delta);
    
            BOOST_SPIRIT_DEBUG_NODES((file)(initialization)(color))
        }
    
      private:
        using Skipper = qi::blank_type;
        qi::rule<It, Ast::File()>                    start;
        qi::rule<It, Ast::File(), Skipper>           file;
        qi::rule<It, Ast::Initialization(), Skipper> initialization;
        qi::rule<It, Ast::Color(), Skipper>          color;
    
        qi::uint_parser<Ast::Id>          _id;
        qi::uint_parser<Ast::Size>        _size;
        qi::uint_parser<Ast::Coord>       _coord;
        qi::uint_parser<Ast::ColorNumber> _colorNumber;
        qi::uint_parser<Ast::Orientation> _orientation;
        qi::int_parser<Ast::Delta>        _delta;
    };
    
    std::string read_file(const std::string& name) {
        std::ifstream ifs(name);
        return std::string(std::istreambuf_iterator<char>(ifs), {});
    }
    
    static Ast::File parse_game(std::string_view input) {
        using SVI = std::string_view::const_iterator;
        static const GameParser<SVI> parser{};
    
        try {
            Ast::File parsed;
            if (qi::parse(input.begin(), input.end(), parser, parsed)) {
                return parsed;
            }
            throw std::runtime_error("Unable to parse game");
        } catch (qi::expectation_failure<SVI> const& ef) {
            std::ostringstream oss;
    
            auto where  = ef.first - input.begin();
            auto sol    = 1 + input.find_last_of("\r\n", where);
            auto lineno = 1 + std::count(input.begin(), input.begin() + sol, '\n');
            auto col    = 1 + where - sol;
            auto llen   = input.substr(sol).find_first_of("\r\n");
    
            oss << "input.txt:" << lineno << ":" << col << " Expected: " << ef.what_ << "\n"
                << " note: " << input.substr(sol, llen) << "\n"
                << " note:"  << std::setw(col) << "" << "^--- here";
            throw std::runtime_error(oss.str());
        }
    }
    
    int main() {
        std::string game_save = read_file("input.txt");
    
        try {
            Ast::File data = parse_game(game_save);
        } catch (std::exception const& e) {
            std::cerr << e.what() << "\n";
        }
    }
    

    Look here for various failure modes and BOOST_SPIRIT_DEBUG ouput:

    enter image description here