2008-09-02 6 views
17

C# a-t-il un support intégré pour l'analyse des chaînes de numéros de pages? Par numéros de page, je veux dire le format que vous pourriez entrer dans une boîte de dialogue d'impression qui est un mélange de virgules et de tirets délimités.Est-ce que C# prend en charge l'analyse syntaxique des chaînes de numéros de pages?

Quelque chose comme ceci:

1,3,5-10,12 

Ce qui serait vraiment agréable est une solution qui m'a redonné une sorte de liste de tous les numéros de page représentés par la chaîne. Dans l'exemple ci-dessus, obtenir une liste en arrière comme ce serait bien:

1,3,5,6,7,8,9,10,12 

Je veux juste éviter de rouler moi-même s'il y a un moyen facile de le faire.

+2

Pour effectuer l'opération inverse, voir http://stackoverflow.com/questions/7688881/convert-list-to-number-range-string – Grhm

Répondre

19

devrait être simple:

foreach(string s in "1,3,5-10,12".Split(',')) 
{ 
    // try and get the number 
    int num; 
    if(int.TryParse(s, out num)) 
    { 
     yield return num; 
     continue; // skip the rest 
    } 

    // otherwise we might have a range 
    // split on the range delimiter 
    string[] subs = s.Split('-'); 
    int start, end; 

    // now see if we can parse a start and end 
    if(subs.Length > 1 && 
     int.TryParse(subs[0], out start) && 
     int.TryParse(subs[1], out end) && 
     end >= start) 
    { 
     // create a range between the two values 
     int rangeLength = end - start + 1; 
     foreach(int i in Enumerable.Range(start, rangeLength)) 
     { 
      yield return i; 
     } 
    } 
} 

Edit: merci pour la correction ;-)

+0

Je suggère deux changements: (1) ajouter 'continuer;' après le premier 'rendement return num;', ce qui vous permettra d'économiser le 'else' et (2) changer la comparaison à' end> ​​= start', ce qui permettra vous pour soutenir les gammes d'articles simples comme '1-1'. –

+0

@Michael Teper - bravo pour le conseil. (1) Je pense que le 'early out' utilisant 'continue' est un pur style de codage. Je préfère ce style «spartiate» mais je trouve que la plupart des développeurs de mon équipe préfèrent un bloc concret «else», surtout quand il ne s'agit que de quelques lignes comme celle-ci. (2) J'ai délibérément évité beaucoup de vérification d'erreurs et autres juste pour garder cet échantillon agréable et simple. Il y a des charges que vous pourriez ajouter - par exemple, si une gamme ne peut pas être analysée, ce code l'ignore, mais une exception peut être préférable, car un saut pourrait entraîner des erreurs manquées en silence. – Keith

7

Il n'a pas de façon intégrée de le faire, mais il serait trivial d'utiliser String.Split. Il suffit de diviser ',' et vous avez une série de chaînes qui représentent soit des numéros de pages, soit des plages. Itérer sur cette série et faire un String.Split de '-'. S'il n'y a pas de résultat, il s'agit d'un numéro de page vierge, alors collez-le dans votre liste de pages. S'il y a un résultat, prenez la gauche et la droite du '-' comme limites et utilisez une simple boucle pour ajouter chaque numéro de page à votre liste finale sur cette plage.

Ne peut pas prendre que 5 minutes à faire, puis peut-être 10 autres pour ajouter quelques contrôles de santé pour lancer des erreurs lorsque l'utilisateur essaie d'entrer des données invalides (comme "1-2-3" ou quelque chose.)

+0

[@Daniel Jennings] (http://stackoverflow.com/questions/40161/does-c-have-built-in-support-for-parsing-page-number-strings#40165) Cela semble être une approche raisonnable. Je pensais juste qu'il valait la peine de s'assurer que Microsoft n'avait pas un PageNumberStringParser quelque part qui traitait tous les cas de bord bizarres. –

5

approche de Keith semble bien. J'ai mis en place une approche plus naïve en utilisant des listes. Ceci a la vérification d'erreur si heureusement devrait ramasser la plupart des problèmes: -

public List<int> parsePageNumbers(string input) { 
    if (string.IsNullOrEmpty(input)) 
    throw new InvalidOperationException("Input string is empty."); 

    var pageNos = input.Split(','); 

    var ret = new List<int>(); 
    foreach(string pageString in pageNos) { 
    if (pageString.Contains("-")) { 
     parsePageRange(ret, pageString); 
    } else { 
     ret.Add(parsePageNumber(pageString)); 
    } 
    } 

    ret.Sort(); 
    return ret.Distinct().ToList(); 
} 

private int parsePageNumber(string pageString) { 
    int ret; 

    if (!int.TryParse(pageString, out ret)) { 
    throw new InvalidOperationException(
     string.Format("Page number '{0}' is not valid.", pageString)); 
    } 

    return ret; 
} 

private void parsePageRange(List<int> pageNumbers, string pageNo) { 
    var pageRange = pageNo.Split('-'); 

    if (pageRange.Length != 2) 
    throw new InvalidOperationException(
     string.Format("Page range '{0}' is not valid.", pageNo)); 

    int startPage = parsePageNumber(pageRange[0]), 
    endPage = parsePageNumber(pageRange[1]); 

    if (startPage > endPage) { 
    throw new InvalidOperationException(
     string.Format("Page number {0} is greater than page number {1}" + 
     " in page range '{2}'", startPage, endPage, pageNo)); 
    } 

    pageNumbers.AddRange(Enumerable.Range(startPage, endPage - startPage + 1)); 
} 
2

Voici quelque chose que j'ai concocté pour quelque chose de similaire.

Il gère les types de plages suivantes:

1  single number 
1-5  range 
-5  range from (firstpage) up to 5 
5-  range from 5 up to (lastpage) 
..  can use .. instead of - 
;,  can use both semicolon, comma, and space, as separators 

Il ne vérifie pas les valeurs en double, de sorte que l'ensemble 1,5, -10 va produire la séquence 1, 5, 1, 2 , 3, 4, 5, 6, 7, 8, 9, 10.

public class RangeParser 
{ 
    public static IEnumerable<Int32> Parse(String s, Int32 firstPage, Int32 lastPage) 
    { 
     String[] parts = s.Split(' ', ';', ','); 
     Regex reRange = new Regex(@"^\s*((?<from>\d+)|(?<from>\d+)(?<sep>(-|\.\.))(?<to>\d+)|(?<sep>(-|\.\.))(?<to>\d+)|(?<from>\d+)(?<sep>(-|\.\.)))\s*$"); 
     foreach (String part in parts) 
     { 
      Match maRange = reRange.Match(part); 
      if (maRange.Success) 
      { 
       Group gFrom = maRange.Groups["from"]; 
       Group gTo = maRange.Groups["to"]; 
       Group gSep = maRange.Groups["sep"]; 

       if (gSep.Success) 
       { 
        Int32 from = firstPage; 
        Int32 to = lastPage; 
        if (gFrom.Success) 
         from = Int32.Parse(gFrom.Value); 
        if (gTo.Success) 
         to = Int32.Parse(gTo.Value); 
        for (Int32 page = from; page <= to; page++) 
         yield return page; 
       } 
       else 
        yield return Int32.Parse(gFrom.Value); 
      } 
     } 
    } 
} 
0

est ici une version légèrement modifiée du code de lassevk qui gère l'opération string.split à l'intérieur du match Regex. Il est écrit comme une méthode d'extension et vous pouvez facilement gérer le problème des doublons en utilisant l'extension Disinct() de LINQ.

/// <summary> 
    /// Parses a string representing a range of values into a sequence of integers. 
    /// </summary> 
    /// <param name="s">String to parse</param> 
    /// <param name="minValue">Minimum value for open range specifier</param> 
    /// <param name="maxValue">Maximum value for open range specifier</param> 
    /// <returns>An enumerable sequence of integers</returns> 
    /// <remarks> 
    /// The range is specified as a string in the following forms or combination thereof: 
    /// 5   single value 
    /// 1,2,3,4,5 sequence of values 
    /// 1-5   closed range 
    /// -5   open range (converted to a sequence from minValue to 5) 
    /// 1-   open range (converted to a sequence from 1 to maxValue) 
    /// 
    /// The value delimiter can be either ',' or ';' and the range separator can be 
    /// either '-' or ':'. Whitespace is permitted at any point in the input. 
    /// 
    /// Any elements of the sequence that contain non-digit, non-whitespace, or non-separator 
    /// characters or that are empty are ignored and not returned in the output sequence. 
    /// </remarks> 
    public static IEnumerable<int> ParseRange2(this string s, int minValue, int maxValue) { 
     const string pattern = @"(?:^|(?<=[,;]))      # match must begin with start of string or delim, where delim is , or ; 
           \s*(        # leading whitespace 
           (?<from>\d*)\s*(?:-|:)\s*(?<to>\d+) # capture 'from <sep> to' or '<sep> to', where <sep> is - or : 
           |         # or 
           (?<from>\d+)\s*(?:-|:)\s*(?<to>\d*) # capture 'from <sep> to' or 'from <sep>', where <sep> is - or : 
           |         # or 
           (?<num>\d+)       # capture lone number 
           )\s*         # trailing whitespace 
           (?:(?=[,;\b])|$)      # match must end with end of string or delim, where delim is , or ;"; 

     Regex regx = new Regex(pattern, RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); 

     foreach (Match m in regx.Matches(s)) { 
      Group gpNum = m.Groups["num"]; 
      if (gpNum.Success) { 
       yield return int.Parse(gpNum.Value); 

      } else { 
       Group gpFrom = m.Groups["from"]; 
       Group gpTo = m.Groups["to"]; 
       if (gpFrom.Success || gpTo.Success) { 
        int from = (gpFrom.Success && gpFrom.Value.Length > 0 ? int.Parse(gpFrom.Value) : minValue); 
        int to = (gpTo.Success && gpTo.Value.Length > 0 ? int.Parse(gpTo.Value) : maxValue); 

        for (int i = from; i <= to; i++) { 
         yield return i; 
        } 
       } 
      } 
     } 
    } 
3

Ci-dessous le code que je viens de mettre ensemble pour cela .. Vous pouvez entrer dans le format ..1-2,5abcd, 6,7,20-15 ,,,,,,

facile d'ajouter sur d'autres formats

private int[] ParseRange(string ranges) 
    { 
     string[] groups = ranges.Split(','); 
     return groups.SelectMany(t => GetRangeNumbers(t)).ToArray(); 
    } 

    private int[] GetRangeNumbers(string range) 
    { 
     //string justNumbers = new String(text.Where(Char.IsDigit).ToArray()); 

     int[] RangeNums = range 
      .Split('-') 
      .Select(t => new String(t.Where(Char.IsDigit).ToArray())) // Digits Only 
      .Where(t => !string.IsNullOrWhiteSpace(t)) // Only if has a value 
      .Select(t => int.Parse(t)) // digit to int 
      .ToArray(); 
     return RangeNums.Length.Equals(2) ? Enumerable.Range(RangeNums.Min(), (RangeNums.Max() + 1) - RangeNums.Min()).ToArray() : RangeNums; 
    } 
0

La réponse que je suis venu avec:

static IEnumerable<string> ParseRange(string str) 
{ 
    var numbers = str.Split(','); 

    foreach (var n in numbers) 
    { 
     if (!n.Contains("-")) 
      yield return n; 
     else 
     { 
      string startStr = String.Join("", n.TakeWhile(c => c != '-')); 
      int startInt = Int32.Parse(startStr); 

      string endStr = String.Join("", n.Reverse().TakeWhile(c => c != '-').Reverse()); 
      int endInt = Int32.Parse(endStr); 

      var range = Enumerable.Range(startInt, endInt - startInt + 1) 
           .Select(num => num.ToString()); 

      foreach (var s in range) 
       yield return s; 
     } 
    } 
} 
1

Vous ne pouvez pas être sûr que vous ayez des cas de test. Dans mon cas je préférerais être délimité par un espace blanc au lieu d'être délimité par des virgules. Cela rend l'analyse un peu plus complexe.

[Fact] 
    public void ShouldBeAbleToParseRanges() 
    { 
     RangeParser.Parse("1").Should().BeEquivalentTo(1); 
     RangeParser.Parse("-1..2").Should().BeEquivalentTo(-1,0,1,2); 

     RangeParser.Parse("-1..2 ").Should().BeEquivalentTo(-1,0,1,2); 
     RangeParser.Parse("-1..2 5").Should().BeEquivalentTo(-1,0,1,2,5); 
     RangeParser.Parse(" -1 .. 2 5").Should().BeEquivalentTo(-1,0,1,2,5); 
    } 

Notez que la réponse de Keith (ou une petite variation) échouent le dernier test où il y a des espaces entre le jeton de gamme. Cela nécessite un tokenizer et un analyseur approprié avec lookahead.

namespace Utils 
{ 
    public class RangeParser 
    { 

     public class RangeToken 
     { 
      public string Name; 
      public string Value; 
     } 

     public static IEnumerable<RangeToken> Tokenize(string v) 
     { 
      var pattern = 
       @"(?<number>-?[1-9]+[0-9]*)|" + 
       @"(?<range>\.\.)"; 

      var regex = new Regex(pattern); 
      var matches = regex.Matches(v); 
      foreach (Match match in matches) 
      { 
       var numberGroup = match.Groups["number"]; 
       if (numberGroup.Success) 
       { 
        yield return new RangeToken {Name = "number", Value = numberGroup.Value}; 
        continue; 
       } 
       var rangeGroup = match.Groups["range"]; 
       if (rangeGroup.Success) 
       { 
        yield return new RangeToken {Name = "range", Value = rangeGroup.Value}; 
       } 

      } 
     } 

     public enum State { Start, Unknown, InRange} 

     public static IEnumerable<int> Parse(string v) 
     { 

      var tokens = Tokenize(v); 
      var state = State.Start; 
      var number = 0; 

      foreach (var token in tokens) 
      { 
       switch (token.Name) 
       { 
        case "number": 
         var nextNumber = int.Parse(token.Value); 
         switch (state) 
         { 
          case State.Start: 
           number = nextNumber; 
           state = State.Unknown; 
           break; 
          case State.Unknown: 
           yield return number; 
           number = nextNumber; 
           break; 
          case State.InRange: 
           int rangeLength = nextNumber - number+ 1; 
           foreach (int i in Enumerable.Range(number, rangeLength)) 
           { 
            yield return i; 
           } 
           state = State.Start; 
           break; 
          default: 
           throw new ArgumentOutOfRangeException(); 
         } 
         break; 
        case "range": 
         switch (state) 
         { 
          case State.Start: 
           throw new ArgumentOutOfRangeException(); 
           break; 
          case State.Unknown: 
           state = State.InRange; 
           break; 
          case State.InRange: 
           throw new ArgumentOutOfRangeException(); 
           break; 
          default: 
           throw new ArgumentOutOfRangeException(); 
         } 
         break; 
        default: 
         throw new ArgumentOutOfRangeException(nameof(token)); 
       } 
      } 
      switch (state) 
      { 
       case State.Start: 
        break; 
       case State.Unknown: 
        yield return number; 
        break; 
       case State.InRange: 
        break; 
       default: 
        throw new ArgumentOutOfRangeException(); 
      } 
     } 
    } 
} 
0

Regex n'est pas efficace comme code suivant. Les méthodes String sont plus efficaces que Regex et devraient être utilisées lorsque cela est possible.

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Text.RegularExpressions; 

namespace ConsoleApplication1 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      string[] inputs = { 
           "001-005/015", 
           "009/015" 
          }; 

      foreach (string input in inputs) 
      { 
       List<int> numbers = new List<int>(); 
       string[] strNums = input.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); 
       foreach (string strNum in strNums) 
       { 
        if (strNum.Contains("-")) 
        { 
         int startNum = int.Parse(strNum.Substring(0, strNum.IndexOf("-"))); 
         int endNum = int.Parse(strNum.Substring(strNum.IndexOf("-") + 1)); 
         for (int i = startNum; i <= endNum; i++) 
         { 
          numbers.Add(i); 
         } 
        } 
        else 
         numbers.Add(int.Parse(strNum)); 
       } 
       Console.WriteLine(string.Join(",", numbers.Select(x => x.ToString()))); 
      } 
      Console.ReadLine(); 

     } 
    } 
} 
0

approche d'une ligne avec Split et Linq

string input = "1,3,5-10,12"; 
IEnumerable<int> result = input.Split(',').SelectMany(x => x.Contains('-') ? Enumerable.Range(int.Parse(x.Split('-')[0]), int.Parse(x.Split('-')[1]) - int.Parse(x.Split('-')[0]) + 1) : new int[] { int.Parse(x) }); 
Questions connexes