¿Existe un tipo de C # para representar un rango de enteros?


105

Necesito almacenar un rango de enteros. ¿Existe un tipo existente para eso en C # 4.0?

Por supuesto, podría escribir mi propia clase con int Fromy int Topropiedades y estructura en la lógica apropiada para asegurar que From <= To. Pero si ya existe un tipo, por supuesto que preferiría usarlo.


2
Creo que la respuesta aceptada debería cambiarse. La respuesta de @ rsenna Enumerable.Rangees, por lo que he visto, la forma de facto de implementar rangos en C # 3.0. Y C # 3.0 existe desde 2007, es decir, 9 años en este momento.
Ehtesh Choudhury

@EhteshChoudhury El OP no menciona nada sobre el manejo de valores discretos, o incluso sobre la validación de esos tipos para un mínimo y un máximo, que es lo que Enumerable.Rangelogra. OP simplemente buscaba una estructura de datos existente que maneja intervalos que pueden tener propiedades de límite inferior y superior, y nada más (además de métodos que imponen ciertos comportamientos).
Sunny Patel

2
@EhteshChoudhury Agregaré mi voz al coro "Don't use Enumerable.Range". Esa es una forma espantosa de resolver el simple problema de mantener un par de valores mínimo / máximo. Cada llamada a Enumerable.Max (IEnumerable <int>) (o Enumerable.Min) itera sobre todo el rango para averiguar los límites. Como han dicho otros, eso podría ser un montón de iteraciones: no estamos hablando de un ajuste de micro rendimiento aquí, estamos hablando de una lentitud paralizante. ¡Ese tipo de programación es la razón por la que .Net recibe un (injustamente) mal nombre por su rendimiento! La respuesta aceptada y respuestas similares son las únicas soluciones prácticas.
Daniel Scott

1
Eso es justo. Enumerable.Rangeocupará mucho más espacio para (1,1000000) frente a un tipo de datos Range para (1,1000000). Leí mal la pregunta la primera vez que se hizo y pensé que era una preguntaEnumerable.Range
Ehtesh Choudhury

1
@EhteshChoudhury Enumerable.Range no tomará 1,1000000 al menos si llamas al método ToList ().
Luis

Respuestas:


136

Encontré que era mejor enrollar el mío. Algunas personas usan Tuples o Points, pero al final desea Rangeque sea extenso y proporcione algunos métodos útiles que se relacionen con a Range. También es mejor si es genérico (¿y si necesita un rango de Doubles, o un rango de alguna clase personalizada?) Por ejemplo:

/// <summary>The Range class.</summary>
/// <typeparam name="T">Generic parameter.</typeparam>
public class Range<T> where T : IComparable<T>
{
    /// <summary>Minimum value of the range.</summary>
    public T Minimum { get; set; }

    /// <summary>Maximum value of the range.</summary>
    public T Maximum { get; set; }

    /// <summary>Presents the Range in readable format.</summary>
    /// <returns>String representation of the Range</returns>
    public override string ToString()
    {
        return string.Format("[{0} - {1}]", this.Minimum, this.Maximum);
    }

    /// <summary>Determines if the range is valid.</summary>
    /// <returns>True if range is valid, else false</returns>
    public bool IsValid()
    {
        return this.Minimum.CompareTo(this.Maximum) <= 0;
    }

    /// <summary>Determines if the provided value is inside the range.</summary>
    /// <param name="value">The value to test</param>
    /// <returns>True if the value is inside Range, else false</returns>
    public bool ContainsValue(T value)
    {
        return (this.Minimum.CompareTo(value) <= 0) && (value.CompareTo(this.Maximum) <= 0);
    }

    /// <summary>Determines if this Range is inside the bounds of another range.</summary>
    /// <param name="Range">The parent range to test on</param>
    /// <returns>True if range is inclusive, else false</returns>
    public bool IsInsideRange(Range<T> range)
    {
        return this.IsValid() && range.IsValid() && range.ContainsValue(this.Minimum) && range.ContainsValue(this.Maximum);
    }

    /// <summary>Determines if another range is inside the bounds of this range.</summary>
    /// <param name="Range">The child range to test</param>
    /// <returns>True if range is inside, else false</returns>
    public bool ContainsRange(Range<T> range)
    {
        return this.IsValid() && range.IsValid() && this.ContainsValue(range.Minimum) && this.ContainsValue(range.Maximum);
    }
}

13
En ese momento, no estaba seguro de que eso IComparablegarantizara la sobrecarga del operador. Tengo garantizado un CompareTométodo, de ahí ese uso.
drharris

14
Resulta que tenía razón al hacer eso: stackoverflow.com/questions/5101378/…
drharris

2
¿Puede ser mejor en structlugar de class?
xmedeko

2
@ Will hey man, no golpee la sabiduría de la documentación básica. :)
drharris

7
¡Muy útil, gracias! ¿Por qué no agregar un constructor que le permita inicializarlo inmediatamente? public Range(T min, T max) { Minimum = min; Maximum = max; }
NateJC

8

Escribí una pequeña clase que podría ser útil para alguien:

    public class Range
    {
        public static List<int> range(int a, int b)
        {
            List<int> result = new List<int>();

            for(int i = a; i <= b; i++)
            {
                result.Add(i);
            }

            return result;
        }

        public static int[] Understand(string input)
        {
            return understand(input).ToArray();
        }

        public static List<int> understand(string input)
        {
            List<int> result = new List<int>();
            string[] lines = input.Split(new char[] {';', ','});

            foreach (string line in lines)
            {
                try
                {
                    int temp = Int32.Parse(line);
                    result.Add(temp);
                }
                catch
                {
                    string[] temp = line.Split(new char[] { '-' });
                    int a = Int32.Parse(temp[0]);
                    int b = Int32.Parse(temp[1]);
                    result.AddRange(range(a, b).AsEnumerable());
                }
            }

            return result;
        }
    }

Entonces solo llama:

Range.understand("1,5-9,14;16,17;20-24")

Y el resultado se ve así:

List<int>
    [0]: 1
    [1]: 5
    [2]: 6
    [3]: 7
    [4]: 8
    [5]: 9
    [6]: 14
    [7]: 16
    [8]: 17
    [9]: 20
    [10]: 21
    [11]: 22
    [12]: 23
    [13]: 24

4
Me gusta mucho la función de comprensión
Dragonborn

Esa understandfunción es genial. Si vi esto en una revisión de nuestro código, sugeriría cambiarle el nombre a Set, porque Range(para mí) suena continuo.
dlw

7

Los rangos y los índices se publican con C # 8.0.

Ahora puedes hacer

string[] names =
{
    "Archimedes", "Pythagoras", "Euclid", "Socrates", "Plato"
};
foreach (var name in names[1..4])
{
    yield return name;
}

Consulte https://blogs.msdn.microsoft.com/dotnet/2018/12/05/take-c-8-0-for-a-spin/ para obtener más detalles.


Este es un concepto diferente y no según el OP deseado.
PepitoSh

Sin embargo, el System.Rangetipo solo acepta int.
snipsnipsnip

Es importante mencionar que solo está disponible desde .NET Core 3.0. No está disponible en .NET Framework.
Sarrus

2

Mejorando en @ andrius-naruševičius una respuesta muy útil para hacerlo más idiomático y fácilmente personalizable

/// <summary>
/// http://stackoverflow.com/questions/5343006/is-there-a-c-sharp-type-for-representing-an-integer-range
/// </summary>
public class Range
{
    readonly static char[] Separators = {','};

    public static List<int> Explode(int from, int to)
    {
        return Enumerable.Range(from, (to-from)+1).ToList();
    }

    public static List<int> Interpret(string input)
    {
        var result = new List<int>();
        var values = input.Split(Separators);

        string rangePattern = @"(?<range>(?<from>\d+)-(?<to>\d+))";
        var regex = new Regex(rangePattern);

        foreach (string value in values)
        {
            var match = regex.Match(value);
            if (match.Success)
            {
                var from = Parse(match.Groups["from"].Value);
                var to = Parse(match.Groups["to"].Value);
                result.AddRange(Explode(from, to));
            }
            else
            {
                result.Add(Parse(value));
            }
        }

        return result;
    }

    /// <summary>
    /// Split this out to allow custom throw etc
    /// </summary>
    private static int Parse(string value)
    {
        int output;
        var ok = int.TryParse(value, out output);
        if (!ok) throw new FormatException($"Failed to parse '{value}' as an integer");
        return output;
    }
}

y las pruebas:

    [Test]
    public void ExplodeRange()
    {
        var output = Range.Explode(5, 9);

        Assert.AreEqual(5, output.Count);
        Assert.AreEqual(5, output[0]);
        Assert.AreEqual(6, output[1]);
        Assert.AreEqual(7, output[2]);
        Assert.AreEqual(8, output[3]);
        Assert.AreEqual(9, output[4]);
    }

    [Test]
    public void ExplodeSingle()
    {
        var output = Range.Explode(1, 1);

        Assert.AreEqual(1, output.Count);
        Assert.AreEqual(1, output[0]);
    }

    [Test]
    public void InterpretSimple()
    {
        var output = Range.Interpret("50");
        Assert.AreEqual(1, output.Count);
        Assert.AreEqual(50, output[0]);
    }

    [Test]
    public void InterpretComplex()
    {
        var output = Range.Interpret("1,5-9,14,16,17,20-24");

        Assert.AreEqual(14, output.Count);
        Assert.AreEqual(1, output[0]);
        Assert.AreEqual(5, output[1]);
        Assert.AreEqual(6, output[2]);
        Assert.AreEqual(7, output[3]);
        Assert.AreEqual(8, output[4]);
        Assert.AreEqual(9, output[5]);
        Assert.AreEqual(14, output[6]);
        Assert.AreEqual(16, output[7]);
        Assert.AreEqual(17, output[8]);
        Assert.AreEqual(20, output[9]);
        Assert.AreEqual(21, output[10]);
        Assert.AreEqual(22, output[11]);
        Assert.AreEqual(23, output[12]);
        Assert.AreEqual(24, output[13]);
    }

    [ExpectedException(typeof (FormatException))]
    [Test]
    public void InterpretBad()
    {
        Range.Interpret("powdered toast man");
    }

2

Escribe un método de extensión como este

 public static class NumericExtentions
    {
        public static bool InRange(this int value, int from, int to)
        {
            if (value >= from && value <= to)
                return true;
            return false;
        }

        public static bool InRange(this double value, double from, double to)
        {
            if (value >= from && value <= to)
                return true;
            return false;
        }
    }

y luego úsalo elegantemente

if (age.InRange(18, 39))
{ 
//Logic
}

1
¿Por qué hacerlo if (value >= from && value <= to) return true;y no return (value >= from && value <= to)?
sdgfsdh

2

Esta implementación, inspirada en la respuesta de @drharris, le permite definir el intervalo matemático adecuado con valores, que pueden ser Inclusivos / Exclusivos.

/// <summary>The Interval class.</summary>
/// <typeparam name="T">Generic parameter.</typeparam>
public class Interval<T> : IEquatable<Interval<T>>
    where T : IComparable<T>, IEquatable<T>
{
    public Interval()
    { }

    public Interval(IntervalValue<T> minimum, IntervalValue<T> maximum)
    {
        this.Minimum = minimum;
        this.Maximum = maximum;
    }

    /// <summary>Minimum value of the interval.</summary>
    public IntervalValue<T>? Minimum { get; set; }

    /// <summary>Maximum value of the interval.</summary>
    public IntervalValue<T>? Maximum { get; set; }

    /// <summary>Presents the Interval in readable format.</summary>
    /// <returns>String representation of the Interval</returns>
    public override string ToString()
    {
        var min = this.Minimum;
        var max = this.Maximum;
        var sb = new StringBuilder();

        if (min.HasValue)
            sb.AppendFormat(min.Value.ToString(IntervalNotationPosition.Left));
        else
            sb.Append("(-∞");

        sb.Append(',');

        if (max.HasValue)
            sb.AppendFormat(max.Value.ToString(IntervalNotationPosition.Right));
        else
            sb.Append("∞)");

        var result = sb.ToString();

        return result;
    }

    /// <summary>Determines if the interval is valid.</summary>
    /// <returns>True if interval is valid, else false</returns>
    public bool IsValid()
    {
        var min = this.Minimum;
        var max = this.Maximum;

        if (min.HasValue && max.HasValue)
            return min.Value.Value.CompareTo(max.Value.Value) <= 0;

        return true;
    }

    /// <summary>Determines if the provided value is inside the interval.</summary>
    /// <param name="x">The value to test</param>
    /// <returns>True if the value is inside Interval, else false</returns>
    public bool ContainsValue(T x)
    {
        if (x == null)
            throw new ArgumentNullException(nameof(x));

        var min = this.Minimum;
        var max = this.Maximum;
        var isValid = this.IsValid();

        if (!isValid)
            throw new InvalidOperationException("Interval is not valid.");

        bool result = true; // (-∞,∞)

        if (min.HasValue)
        {
            if (min.Value.Type == IntervalValueType.Exclusive)
                result &= min.Value.Value.CompareTo(x) < 0;
            else if (min.Value.Type == IntervalValueType.Inclusive)
                result &= min.Value.Value.CompareTo(x) <= 0;
            else
                throw new NotSupportedException();
        }

        if (max.HasValue)
        {
            if (max.Value.Type == IntervalValueType.Exclusive)
                result &= max.Value.Value.CompareTo(x) > 0;
            else if (max.Value.Type == IntervalValueType.Inclusive)
                result &= max.Value.Value.CompareTo(x) >= 0;
            else
                throw new NotSupportedException();
        }

        return result;
    }

    public bool Equals(Interval<T> other)
    {
        if (other == null)
            return false;

        if (ReferenceEquals(this, other))
            return true;

        return this.Minimum?.Equals(other.Minimum) == true
            && this.Maximum?.Equals(other.Maximum) == true;
    }

    public override bool Equals(object obj)
    {
        return this.Equals(obj as Interval<T>);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            int hash = (int)2166136261;

            hash = hash * 16777619 ^ this.Minimum?.GetHashCode() ?? 0;
            hash = hash * 16777619 ^ this.Maximum?.GetHashCode() ?? 0;

            return hash;
        }
    }
}

public struct IntervalValue<T> : IEquatable<IntervalValue<T>>
    where T : IComparable<T>, IEquatable<T> //, IFormattable
{
    private readonly T value;
    private readonly IntervalValueType type;

    public IntervalValue(T value, IntervalValueType type)
    {
        if (value == null)
            throw new ArgumentNullException(nameof(value));

        this.value = value;
        this.type = type;
    }

    public T Value
    {
        get { return this.value; }
    }

    public IntervalValueType Type
    {
        get { return this.type; }
    }

    public bool Equals(IntervalValue<T> other)
    {
        return this.value.Equals(other.value)
            && this.type == other.type;
    }

    public override bool Equals(object obj)
    {
        return obj is IntervalValue<T> && this.Equals((IntervalValue<T>)obj);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            int hash = (int)2166136261;

            hash = hash * 16777619 ^ this.value.GetHashCode();
            hash = hash * 16777619 ^ this.type.GetHashCode();

            return hash;
        }
    }

    internal string ToString(IntervalNotationPosition position)
    {
        var notation = this.Type.ToString(position);

        switch (position)
        {
            case IntervalNotationPosition.Left:
                return string.Format("{0}{1}", notation, this.Value);

            case IntervalNotationPosition.Right:
                return string.Format("{0}{1}", this.Value, notation);

            default:
                throw new NotSupportedException();
        }
    }
}

internal static class IntervalValueTypeExtensions
{
    public static string ToString(this IntervalValueType type, IntervalNotationPosition position)
    {
        switch (position)
        {
            case IntervalNotationPosition.Left:
                switch (type)
                {
                    case IntervalValueType.Inclusive: return "[";
                    case IntervalValueType.Exclusive: return "(";

                    default:
                        throw new NotSupportedException();
                }

            case IntervalNotationPosition.Right:
                switch (type)
                {
                    case IntervalValueType.Inclusive: return "]";
                    case IntervalValueType.Exclusive: return ")";

                    default:
                        throw new NotSupportedException();
                }
                break;

            default:
                throw new NotSupportedException();
        }
    }
}

public enum IntervalValueType
{
    Inclusive,
    Exclusive
}

public enum IntervalNotationPosition
{
    Left,
    Right
}

1

Además, aquí hay una tangente un poco diferente, pero a veces los rangos son útiles solo para iterar sobre ellos, un poco como se acostumbra hacer en Python. En ese caso, el System.Linqespacio de nombres define un static IEnumerable<int> Range(Int32, Int32)método que, como sugiere la firma,

genera una secuencia de números enteros dentro de un rango especificado

Consulte la documentación y los ejemplos en MSDN.


@Seth no debería, ya que el rango Enumerable acepta comenzar y contar.
Rashid

@Rashid es bastante justo, pero realmente no lo considero un problema. Sé que OP pidió una "clase con int Fromy int To", pero estoy bastante seguro de que este no era un requisito específico, sino algo para ilustrar su punto. Para cualquier persona interesada, por supuesto, el valor del countparámetro se puede determinar muy fácilmente, dado que ya tiene int Fromy int Tovariables.
Gaboik1

1

Hay un Enumerable.Rangemétodo pero éste acepta starty countcomo parámetro. Para que funcione como lo desea starty end, la endfórmula seríaend - start + 1

Uso:

Enumerable.Range(start, end - start + 1).ToList()


0

Como también me faltaban intervalos en C #, implementé una clase Interval completamente genérica que incluso puede ocuparse de intervalos con tipos más complejos, por ejemplo, un intervalo entre dos DateTime, lo que implica TimeSpandurante los cálculos.

Un caso de uso de ejemplo, donde un elemento GUI representa un intervalo de tiempo:

// Mockup of a GUI element and mouse position.
var timeBar = new { X = 100, Width = 200 };
int mouseX = 180;

// Find out which date on the time bar the mouse is positioned on,
// assuming it represents whole of 2014.
var timeRepresentation = new Interval<int>( timeBar.X, timeBar.X + timeBar.Width );
DateTime start = new DateTime( 2014, 1, 1 );
DateTime end = new DateTime( 2014, 12, 31 );
var thisYear = new Interval<DateTime, TimeSpan>( start, end );
DateTime hoverOver = timeRepresentation.Map( mouseX, thisYear );

// If the user clicks, zoom in to this position.
double zoomLevel = 0.5;
double zoomInAt = thisYear.GetPercentageFor( hoverOver );
Interval<DateTime, TimeSpan> zoomed = thisYear.Scale( zoomLevel, zoomInAt );

// Iterate over the interval, e.g. draw labels.
zoomed.EveryStepOf( TimeSpan.FromDays( 1 ), d => DrawLabel( d ) );

Para obtener una representación más amplia de la funcionalidad admitida, consulte las pruebas unitarias .

Debajo de las cubiertas, utiliza árboles de expresión para compilar operaciones de operador de tipo en tiempo de ejecución , que se almacenan en caché, por lo que solo hay un costo la primera vez que se inicializa el tipo.


-2

¿Qué tal una estructura ?


2
No usaría una estructura a menos que supiera que los valores mínimo y máximo nunca cambiarían una vez que se crea una instancia.
IAbstract

Responde a la pregunta "ya existe". El tipo de estructura existe específicamente para tipos como el en cuestión.
James Sumners

3
Struct no es un tipo en sí mismo. Básicamente, estás diciendo "no, crea tu propio tipo", esa es una respuesta legítima
Robert Levy

2
Las primeras tres palabras de la documentación vinculada no concuerdan con usted: "El tipo de estructura ..."
James Sumners

Tiendo a estar de acuerdo con jsumners. Una estructura es una forma fácil y directa de hacer esto, y no produce 2 páginas de códigos que deban mantenerse más adelante para un problema tan simple. También puede usar un Par <...> para lo mismo. Microsoft rompe su propia "regla" acerca de que las estructuras son inmutables en varios lugares.
Tom
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.