2008-09-02 7 views
17

¿Tiene C# compatibilidad incorporada para analizar cadenas de números de página? Por números de página, me refiero al formato que puede ingresar en un diálogo de impresión que es una mezcla de coma y delimitado por guiones.¿Tiene C# compatibilidad incorporada para analizar cadenas de número de página?

Algo como esto:

1,3,5-10,12 

Lo que sería realmente agradable es una solución que me devolvió una especie de lista de todos los números de página representados por la cadena. En el ejemplo anterior, obteniendo una lista espalda como esta sería bueno:

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

sólo quiero evitar rodar mi propia si hay una manera fácil de hacerlo.

+2

para realizar la operación inversa, ver http://stackoverflow.com/questions/7688881/convert-list-to-number-range-string – Grhm

Respuesta

19

debe ser 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; 
     } 
    } 
} 

Editar: gracias por la solución ;-)

+0

Sugiero dos cambios: (1) agregue 'continue;' después del primer 'yield return num;', que le ahorrará la necesidad del 'else' y (2) cambiará la comparación a' end> ​​= start', lo que habilitará para admitir rangos de elementos individuales como '1-1'. –

+0

@Michael Teper - aplausos por el consejo. (1) Creo que el 'early out' que utiliza 'continue' es una cosa pura de estilo de codificación. Prefiero ese estilo 'spartan' pero encuentro que la mayoría de los desarrolladores de mi equipo prefieren un bloque concreto 'else', especialmente cuando son solo unas pocas líneas como esta. (2) Evité deliberadamente muchas comprobaciones de errores y similares para mantener esta muestra agradable y simple. Hay muchas cosas que podría agregar: por ejemplo, si no se puede analizar un rango, este código simplemente se saltará, pero es posible que se prefiera algún tipo de excepción, ya que al omitirlo se pueden producir errores que se pierden por error. – Keith

7

No tiene una forma incorporada de hacer esto, pero sería trivial hacerlo usando String.Split.

Simplemente divida en ',' luego tiene una serie de cadenas que representan números de página o rangos. Iteramos sobre esa serie y hacemos un String.Split de '-'. Si no hay un resultado, es un número de página simple, así que guárdelo en su lista de páginas. Si hay un resultado, tome la izquierda y la derecha de '-' como los límites y use un bucle for para agregar cada número de página a su lista final sobre ese rango.

No se pueden tomar más de 5 minutos, quizás otros 10 para agregar algunos controles de cordura para arrojar errores cuando el usuario intenta ingresar datos no válidos (como "1-2-3" o algo así)

+0

[@Daniel Jennings] (http://stackoverflow.com/questions/40161/does-c-have-built-in-support-for-parsing-page-number-strings#40165) Parece un enfoque razonable. Solo pensé que valía la pena asegurarme de que Microsoft no tuviera un PageNumberStringParser allí que manejara todos los casos extraños. –

5

enfoque de Keith parece agradable. Arreglé un enfoque más ingenuo usando listas. Esto tiene una comprobación de errores, así que con suerte debería recoger la mayoría de los problemas: -

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

Aquí hay algo que preparé para algo similar.

Se ocupa de los siguientes tipos de gamas:

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 

No comprueba para valores duplicados, así que el conjunto 1,5, -10 producirá la secuencia 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

Aquí es una versión ligeramente modificada del código de lassevk que se encarga de la operación string.split dentro del partido expresión regular. Está escrito como un método de extensión y puede manejar fácilmente el problema de duplicados utilizando la extensión 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

A continuación se muestra el código que acabo de reunir para hacer esto .. Puede ingresar en el formato como ...1-2,5abcd, 6,7,20-15 ,,,,,,

fácil de complemento para otros formatos

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 respuesta que se me ocurrió:

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

No puede estar seguro hasta que tenga casos de prueba. En mi caso, preferiría delimitar el espacio en blanco en lugar de estar delimitado por comas. Hace que el análisis sea un poco más complejo.

[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); 
    } 

Tenga en cuenta que la respuesta de Keith (o una pequeña variación) se producirá un error de la última prueba donde hay espacios en blanco entre el token gama. Esto requiere un tokenizer y un analizador apropiado con 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 no es eficiente como el código siguiente. Los métodos de cadena son más eficientes que Regex y se deben usar cuando sea posible.

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

Un método de acuerdo con Split y 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) }); 
Cuestiones relacionadas