Is There a C# Type for Representing an Integer Range

Is there a C# type for representing an integer Range?

I found it best to roll my own. Some people use Tuples or Points, but in the end you want your Range to be extensive and provide some handy methods that relate to a Range. It's also best if generic (what if you need a range of Doubles, or a range of some custom class?) For example:

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

Is there a standard class to represent a “range” in .net?

This just changed with .Net Core 3.0 see System.Range.

C# 8 also has language support for creating ranges.

See also the "What is Range and Index types in c# 8?" Stackoverflow Question.

Notes these only support ranges of integers, and there is no support for ranges of double or floats.

List of integer ranges in C#

For this kind of thing I usually end up writing my own classes. Here's what I would do for this:

First, the Range class, which has a Begin, End, and Tag. It also has some helper methods to simplify querying for ranges that are overlapping and adjacent, or for doing case-insensitive tag comparison, and for outputting a string value:

class Range
{
public int Begin { get; set; }
public int End { get; set; }
public string Tag { get; set; }

public bool CombineWith(Range other)
{
Range combinedRange;
if (TryCombine(this, other, out combinedRange))
{
this.Begin = combinedRange.Begin;
this.End = combinedRange.End;
return true;
}

return false;
}

public bool IsAdjacentTo(Range other)
{
return AreAdjacent(this, other);
}

public bool OverlapsWith(Range other)
{
return AreOverlapping(this, other);
}

public bool ContainsIndex(int index)
{
return this.Begin <= index && this.End >= index;
}

public bool TagEquals(string tag)
{
if (this.Tag == null) return tag == null;
return this.Tag.Equals(tag, StringComparison.OrdinalIgnoreCase);
}

public static bool TryCombine(Range first, Range second, out Range combined)
{
combined = new Range();

if (first == null || second == null) return false;
if (!TagsEqual(first, second)) return false;
if (!AreAdjacent(first, second) && !AreOverlapping(first, second)) return false;

combined.Begin = Math.Min(first.Begin, second.Begin);
combined.End = Math.Max(first.End, second.End);
combined.Tag = first.Tag;

return true;
}

public static bool AreAdjacent(Range first, Range second)
{
if (first == null || second == null) return false;
if (!Range.TagsEqual(first, second)) return false;

return (first.Begin == second.End + 1) ||
(first.End == second.Begin - 1);
}

public static bool AreOverlapping(Range first, Range second)
{
if (first == null || second == null) return false;

return (first.Begin >= second.Begin && first.Begin <= second.End) ||
(first.End >= second.Begin && first.End <= second.End);
}

public static bool TagsEqual(Range first, Range second)
{
if (first == null || second == null) return false;
return first.TagEquals(second.Tag);
}

public override string ToString()
{
return $"begin: {Begin}, end: {End}, tag: {Tag}";
}
}

Next is your IntRangeArray class, which manages the adding and removing of items in the a list of Range objects:

class IntRangeArray
{
private readonly List<Range> ranges = new List<Range>();

public bool Add(int index, string tag)
{
return AddRange(index, index, tag);
}

public bool AddRange(IEnumerable<int> indexes, string tag)
{
if (indexes == null || string.IsNullOrWhiteSpace(tag)) return false;

bool result = true;

foreach (var index in indexes)
{
if (!Add(index, tag)) result = false;
}

return result;
}

public bool AddRange(Tuple<int, int> range, string tag)
{
return AddRange(range.Item1, range.Item2, tag);
}

public bool AddRange(int begin, int end, string tag)
{
if (begin < 0 || end < 0 || string.IsNullOrWhiteSpace(tag)) return false;

var newRange = new Range {Begin = begin, End = end, Tag = tag};
var overlappingRanges = ranges.Where(r => r.OverlapsWith(newRange)).ToList();
var adjacentRanges = ranges.Where(r => r.IsAdjacentTo(newRange)).ToList();

if (overlappingRanges.Any())
{
if (!overlappingRanges.All(r => r.TagEquals(newRange.Tag)))
{
return false;
}

foreach (var overlappingRange in overlappingRanges)
{
newRange.CombineWith(overlappingRange);
ranges.Remove(overlappingRange);
}
}

foreach (var adjacentRange in adjacentRanges)
{
newRange.CombineWith(adjacentRange);
ranges.Remove(adjacentRange);
}

ranges.Add(newRange);
return true;
}

public string At(int index)
{
var matchingRange = ranges.SingleOrDefault(r => r.ContainsIndex(index));
return matchingRange?.ToString() ?? $"No item exists at {index}";
}

public void Remove(int index)
{
var matchingRange = ranges.SingleOrDefault(r => r.ContainsIndex(index));
if (matchingRange == null) return;

if (matchingRange.Begin == matchingRange.End)
{
ranges.Remove(matchingRange);
}
else if (index == matchingRange.Begin)
{
matchingRange.Begin += 1;
}
else if (index == matchingRange.End)
{
matchingRange.End -= 1;
}
else
{
// Split the range by creating a new one for the beginning
var newRange = new Range
{
Begin = matchingRange.Begin,
End = index - 1,
Tag = matchingRange.Tag
};

matchingRange.Begin = index + 1;
ranges.Add(newRange);
}
}

public void RemoveWithTag(string tag)
{
ranges.RemoveAll(r => r.TagEquals(tag));
}

public string TagOf(int index)
{
var matchingRange = ranges.SingleOrDefault(r => r.ContainsIndex(index));
return matchingRange == null ? $"No item exists at {index}" : matchingRange.Tag;
}

public override string ToString()
{
if (ranges == null || !ranges.Any()) return "No items exist.";

ranges.Sort((x, y) => x.Begin.CompareTo(y.Begin));
var output = new StringBuilder();

for(int i = 0; i < ranges.Count; i++)
{
output.AppendLine($"[{i}] {ranges[i]}");
}

return output.ToString();
}
}

To test it out, I just copied and pasted your code sample above:

private static void Main()
{
/* int is the integer type, while string is the "tag" object */
var animals = new IntRangeArray();

animals.Add(1, "dog");
Console.WriteLine(animals);

animals.Add(2, "dog");
Console.WriteLine(animals);

/* AddRange with C#7.0 ValueTuple */
animals.AddRange(Tuple.Create(4, 14), "dog");
Console.WriteLine(animals);

animals.Add(3, "dog");
Console.WriteLine(animals);

animals.AddRange(new int[] { 15, 17, 18, 19 }, "dog");
Console.WriteLine(animals);

animals.Add(16, "cat");
Console.WriteLine(animals);

animals.Remove(8);
Console.WriteLine(animals);

Console.WriteLine(animals.At(11));

animals.RemoveWithTag("dog");
Console.WriteLine(animals);

Console.WriteLine(animals.TagOf(16));

Console.WriteLine("\nDone!\nPress any key to exit...");
Console.ReadKey();
}

And the output is as you expected (except there is one item different, but that was a bug on your side):

Sample Image

On what bases the size and range of any datatype is decided?

In assembler size in bits of the types is defined by the architecture that has to work with the numbers. Most current architectures group bits in 8-bit bytes, and double the size of the number when going to the next type: 8, 16, 32, 64, 128... but not all architectures support all the types, and some old architectures had weird types (14 bit integers, for example).

When you use a programming language, the language abstracts the lower levels, and the types in the language are defined to abstract the lower level types. Depending on the language the types might differ: char, short int, int some proposed short, tall, grande. In some cases it will be exactly defined (in Java or C# an int is exactly 32bits, and a long is 64bits), while in others like C/C++ only the relationships among the types are defined (long is not smaller than int which in turn is not smaller than short, then char...)

The maximum number that a given unsigned type can hold can be calculated as 2^N-1, so for a 32 bit unsigned int, the maximum value is 4294967295. The reason for the -1 part is that there are 2^N distinct numbers, one of which is 0, so only 2^N-1 are left for non-zero values.

When it comes to signed integer types, again the architecture has much to say. Most current architectures use two's-complement. In those platforms the highest signed value for an N bit signed integer will be 2^(N-1)-1 and the most negative number will be -2^(N-1). Some older architectures reserved one bit for the sign and then stored the actual number, in which case the negative range would be reduced by one (this approach allows for two zero values: 0 and -0).

As to your question, you will have to pick the language first. Then, in the case of C or C++, you will have to pick your platform, and even the compiler. While in most cases the types are directly related (int is 32 bits in most 32 and 64bit architectures in C++), that is not guaranteed, and some types might differ (long might represent a 32 or 64bit integer type). As of the actual size of a type in C++ besides char that is guaranteed to have CHAR_BITS bits, you can use sizeof(type)*CHAR_BITS to get the size in bits.

Specifying Ranges in C#

You could try making some little anonymous (or not-so-anonymous, for re-use) functions:

Func<int, bool> range1 = i => (1000000 >= i) && (i <= 50000000);
Func<int, bool> range2 = i => (50000001 >= i) && (i <= 10000000);
Func<int, bool> limit = i => i <= 10000000;

var test = 2000000;

if(limit(test) && range1(test))
{
result = 10;
}
else if(limit(test) && range2(test))
{
result = 20;
}

Range of integers that can be expressed precisely as floats / doubles

Since you're asking about IEEE floating-point types, the language does not matter.

#include <iostream>
using namespace std;

int main(){

float f0 = 16777215.; // 2^24 - 1
float f1 = 16777216.; // 2^24
float f2 = 16777217.; // 2^24 + 1

cout << (f0 == f1) << endl;
cout << (f1 == f2) << endl;

double d0 = 9007199254740991.; // 2^53 - 1
double d1 = 9007199254740992.; // 2^53
double d2 = 9007199254740993.; // 2^53 + 1

cout << (d0 == d1) << endl;
cout << (d1 == d2) << endl;
}

Output:

0
1
0
1

So the limit for float is 2^24. And the limit for double is 2^53. Negatives are the same since the only difference is the sign bit.

Is range variable possible in C# (.NET)

Another option would be to define your own data type to represent age values. This type could enforce the constraints.

struct Age
{
const int MinAge = 0;
const int MaxAge = 75;

readonly byte value;

public int Value { get { return value; } }

private Age(int value) {
this.value = (byte) Math.Max(MinAge, Math.Min(MaxAge, value));
}

public static implicit operator Age(int value) {
// Throw here if value is out of range, maybe?
return new Age(value);
}

public static implicit operator int(Age age) {
return age.value;
}
}

//usage:
Age childAge = 12; // 12
Age motherAge = 100; // 75

Edit: I would point out that it is generally considered bad practice to have "lossy" conversions exposed as implicit casts. I should have made the operator int(Age) conversion explicit instead. This would require writing an explicit cast Age age = (Age) 100; which advertises to the consumer of the API that the cast isn't "identity preserving". This is similar to a cast from long to int or double to float, there is a loss of range/precision so the language requires you be explicit about it to demonstrate that you understand what you are doing.



Related Topics



Leave a reply



Submit