Analyser les ints / int-ranges séparés par des virgules en C ++

Aug 17 2020

Étant donné une chaîne en C ++ contenant des plages et des nombres uniques du genre:

"2,3,4,7-9"

Je veux l'analyser en un vecteur de la forme:

2,3,4,7,8,9

Si les nombres sont séparés par un, -je veux pousser tous les nombres de la plage. Sinon, je veux pousser un seul numéro.

J'ai essayé d'utiliser ce morceau de code:

const char *NumX = "2,3,4-7";
std::vector<int> inputs;
std::istringstream in( NumX );
std::copy( std::istream_iterator<int>( in ), std::istream_iterator<int>(),
           std::back_inserter( inputs ) );

Le problème était que cela ne fonctionnait pas pour les gammes. Il n'a fallu que les nombres de la chaîne, pas tous les nombres de la plage.

Réponses

3 d4rk4ng31 Aug 17 2020 at 08:32

En dehors de @J. L'excellent exemple de Schultke, je suggère l'utilisation des regex de la manière suivante:

#include <algorithm>
#include <iostream>
#include <regex>
#include <string>
#include <vector>

void process(std::string str, std::vector<int>& num_vec) {
    str.erase(--str.end());
    for (int i = str.front() - '0'; i <= str.back() - '0'; i++) {
        num_vec.push_back(i);                                                     
    }
}

int main() {
    std::string str("1,2,3,5-6,7,8");
    str += "#";
    std::regex vec_of_blocks(".*?\,|.*?\#");
    auto blocks_begin = std::sregex_iterator(str.begin(), str.end(), vec_of_blocks);
    auto blocks_end = std::sregex_iterator();
    std::vector<int> vec_of_numbers;
    for (std::sregex_iterator regex_it = blocks_begin; regex_it != blocks_end; regex_it++) {
        std::smatch match = *regex_it;
        std::string block = match.str();
        if (std::find(block.begin(), block.end(), '-') != block.end()) {
            process(block, vec_of_numbers);
        }
        else {
            vec_of_numbers.push_back(std::atoi(block.c_str()));
        }
    }
    return 0;
}

Bien sûr, vous avez encore besoin d'un peu de validation, cependant, cela vous permettra de démarrer.

5 JanSchultke Aug 17 2020 at 07:54

Votre problème se compose de deux problèmes distincts:

  1. diviser la chaîne en plusieurs chaînes à ,
  2. ajouter des nombres ou des plages de nombres à un vecteur lors de l'analyse de chaque chaîne

Si vous divisez d'abord la chaîne entière par une virgule, vous n'aurez pas à vous soucier de la diviser en un trait d'union en même temps. C'est ce que vous appelleriez une approche Divide-and-Conquer .

Fractionnement à ,

Cette question devrait vous indiquer comment diviser la chaîne par une virgule.

Analyse et ajout à std::vector<int>

Une fois que vous avez divisé la chaîne par une virgule, il vous suffit de transformer les plages en nombres individuels en appelant cette fonction pour chaque chaîne:

#include <vector>
#include <string>

void push_range_or_number(const std::string &str, std::vector<int> &out) {
    size_t hyphen_index;
    // stoi will store the index of the first non-digit in hyphen_index.
    int first = std::stoi(str, &hyphen_index);
    out.push_back(first);

    // If the hyphen_index is the equal to the length of the string,
    // there is no other number.
    // Otherwise, we parse the second number here:
    if (hyphen_index != str.size()) {
        int second = std::stoi(str.substr(hyphen_index + 1), &hyphen_index);
        for (int i = first + 1; i <= second; ++i) {
            out.push_back(i);
        }
    }
}

Notez que le fractionnement au niveau d'un trait d'union est beaucoup plus simple car nous savons qu'il peut y avoir au plus un trait d'union dans la chaîne. std::string::substrest le moyen le plus simple de le faire dans ce cas. Sachez que cela std::stoipeut lever une exception si l'entier est trop grand pour tenir dans un fichier int.

2 ArminMontigny Aug 17 2020 at 15:03

Toutes les très belles solutions à ce jour. En utilisant C ++ et regex modernes, vous pouvez créer une solution tout-en-un avec seulement très peu de lignes de code.

Comment? Tout d'abord, nous définissons une expression régulière qui correspond à un entier OU à une plage d'entiers. Il ressemblera à ceci

((\d+)-(\d+))|(\d+)

Vraiment très simple. D'abord la gamme. Donc, quelques chiffres, suivis d'un trait d'union et d'autres chiffres. Puis l'entier simple: quelques chiffres. Tous les chiffres sont mis en groupes. (un appareil dentaire). Le trait d'union ne fait pas partie d'un groupe correspondant.

Tout cela est si simple qu'aucune autre explication n'est nécessaire.

Ensuite, nous appelons std::regex_searchen boucle, jusqu'à ce que toutes les correspondances soient trouvées.

Pour chaque correspondance, nous vérifions s'il existe des sous-correspondances, c'est-à-dire une plage. Si nous avons des sous-correspondances, une plage, alors nous ajoutons les valeurs entre les sous-correspondances (incluses) au résultat std::vector.

Si nous n'avons qu'un entier simple, alors nous ajoutons uniquement cette valeur.

Tout cela donne un programme très simple et facile à comprendre:

#include <iostream>
#include <string>
#include <vector>
#include <regex>

const std::string test{ "2,3,4,7-9" };

const std::regex re{ R"(((\d+)-(\d+))|(\d+))" };
std::smatch sm{};

int main() {
    // Here we will store the resulting data
    std::vector<int> data{};

    // Search all occureences of integers OR ranges
    for (std::string s{ test }; std::regex_search(s, sm, re); s = sm.suffix()) {

        // We found something. Was it a range?
        if (sm[1].str().length())

            // Yes, range, add all values within to the vector  
            for (int i{ std::stoi(sm[2]) }; i <= std::stoi(sm[3]); ++i) data.push_back(i);
        else
            // No, no range, just a plain integer value. Add it to the vector
            data.push_back(std::stoi(sm[0]));
    }
    // Show result
    for (const int i : data) std::cout << i << '\n';
    return 0;
}

Si vous avez d'autres questions, je serai ravi d'y répondre.


Langage: C ++ 17 Compilé et testé avec MS Visual Studio 19 Community Edition

JohnPark Aug 17 2020 at 08:42

Pensez à pré-traiter votre chaîne de nombres et à les diviser. Dans le code suivant, transform()convertirait l'un des délimitations, , -et +, en un espace afin que l' std::istream_iteratoranalyse int réussie.

#include <cstdlib>
#include <algorithm>
#include <string>
#include <vector>
#include <iostream>
#include <sstream>

int main(void)
{
    std::string nums = "2,3,4-7,9+10";
    const std::string delim_to_convert = ",-+";  // , - and +
    std::transform(nums.cbegin(), nums.cend(), nums.begin(),
            [&delim_to_convert](char ch) {return (delim_to_convert.find(ch) != string::npos) ? ' ' : ch; });

    std::istringstream ss(nums);
    auto inputs = std::vector<int>(std::istream_iterator<int>(ss), {});

    exit(EXIT_SUCCESS);
}

Notez que le code ci-dessus ne peut fractionner que des délimitations de longueur de 1 octet. Vous devriez vous référer à la réponse @ d4rk4ng31 si vous avez besoin de délimitations plus complexes et plus longues.