Dates, numbers and ranges

edit

Start and end of a period

var date = new DateTime(2026, 10, 29, 14, 30, 15, 250); // A Thursday

// StartOfWeek keeps the time of day
Assert.Equal(new DateTime(2026, 10, 26, 14, 30, 15, 250), date.StartOfWeek(DayOfWeek.Monday));

// StartOfMonth and StartOfYear return midnight, unless keepTime is true
Assert.Equal(new DateTime(2026, 10, 1), date.StartOfMonth());
Assert.Equal(new DateTime(2026, 10, 1, 14, 30, 15, 250), date.StartOfMonth(keepTime: true));
Assert.Equal(new DateTime(2026, 1, 1), date.StartOfYear());

// EndOfMonth returns midnight of the last day of the month
Assert.Equal(new DateTime(2026, 10, 31), date.EndOfMonth());

// TruncateMilliseconds removes everything below the second
Assert.Equal(new DateTime(2026, 10, 29, 14, 30, 15), date.TruncateMilliseconds());
MethodReturns
StartOfWeek(DayOfWeek)The last given day of the week on or before the date, at the same time of day
StartOfWeek()The same, with the first day of the week of the current culture
StartOfMonth(), StartOfYear()The first day of the month or the year, at midnight
StartOfMonth(keepTime: true), StartOfYear(keepTime: true)The first day of the month or the year, at the same time of day
EndOfMonth()The last day of the month, at midnight
TruncateMilliseconds()The date without its fraction of a second
  • StartOfWeek keeps the time of day: use .Date on the result to get midnight.
  • EndOfMonth returns midnight of the last day, not the last moment of the month. To select the dates of a month, compare with the start of the next month instead: date >= start && date < start.AddMonths(1).
  • The results keep the Kind of the date (Utc, Local or Unspecified).
  • StartOfWeek() depends on the current culture: Monday in most of Europe, Sunday in the United States. Pass the day explicitly when the result must not depend on the user's settings.

Ranges

Range<T> holds two bounds, From and To, of any type that implements IComparable<T>, and checks whether a value or another range is between them. Range.Create infers the type of the bounds:

var range = Range.Create(1, 10);

Assert.True(range.IsInRangeInclusive(10));       // 1 <= value <= 10
Assert.False(range.IsInRangeExclusive(10));      // 1 < value < 10
Assert.True(range.IsInRangeLowerInclusive(1));   // 1 <= value < 10
Assert.False(range.IsInRangeLowerInclusive(10));
Assert.True(range.IsInRangeUpperInclusive(10));  // 1 < value <= 10

// A range is in another range when both of its bounds are
Assert.True(range.IsInRangeInclusive(Range.Create(2, 5)));
Assert.False(range.IsInRangeInclusive(Range.Create(5, 15)));
MethodLower boundUpper bound
IsInRangeInclusiveIncludedIncluded
IsInRangeExclusiveExcludedExcluded
IsInRangeLowerInclusiveIncludedExcluded
IsInRangeUpperInclusiveExcludedIncluded
  • Each method has an overload that takes a Range<T>: it returns true when both bounds of that range pass the check.
  • For a reference type, such as string, a null bound is not checked: the range is open on that side.
  • The constructor throws an ArgumentException when From is greater than To.

Overlaps checks whether two ranges have a value in common, and Intersect returns the values they have in common, or null when they have none. Both include the bounds:

var range = Range.Create(1, 10);

Assert.True(range.Overlaps(Range.Create(5, 15)));
Assert.True(range.Overlaps(Range.Create(10, 20))); // The bounds are inclusive
Assert.False(range.Overlaps(Range.Create(11, 20)));

Assert.Equal(Range.Create(5, 10), range.Intersect(Range.Create(5, 15)));
Assert.Null(range.Intersect(Range.Create(11, 20)));
  • Range<T> is a read-only struct. Two ranges are equal when their bounds are equal.

Meziantou.Framework.Range and Meziantou.Framework.Range<T> have the same name as System.Range, the type of the .. operator. When both namespaces are imported, qualify the name or add a using alias.

Copy the sign of a number

MakeSameSignAs(sign) returns the absolute value of the number with the sign of another one: 5.MakeSameSignAs(-1) is -5, and (-5).MakeSameSignAs(3) is 5. It exists for int, long, float, double and decimal.

A zero sign is positive, so (-5).MakeSameSignAs(0) is 5. For float, double and decimal, which have a negative zero, a negative zero sign gives a negative result, like Math.CopySign.

For int and long, int.MinValue.MakeSameSignAs(-1) is int.MinValue, but a positive or zero sign throws an OverflowException: the absolute value of int.MinValue is not an int.

Random values

RandomExtensions adds methods to Random for the types it does not cover, and for picking values from a list:

// A seed gives the same values on every run; use Random.Shared otherwise
var random = new Random(42);

// The minimum is inclusive and the maximum is exclusive: a die roll is between 1 and 6
var die = random.NextInt32(1, 7);
Assert.InRange(die, 1, 6);

string[] colors = ["red", "green", "blue"];
Assert.Contains(random.NextFromList(colors), colors);

var code = random.NextString(length: 6, chars: "ABCDEFGHJKLMNPQRSTUVWXYZ23456789");
Assert.HasCount(6, code);

var date = random.NextDateTime(new DateTime(2026, 1, 1), new DateTime(2027, 1, 1));
Assert.Equal(2026, date.Year);
MethodReturns
NextBoolean()true or false
NextByte(), NextSByte(), NextInt16(), NextUInt16(), NextUInt32(), NextUInt64()Any value of the type, MinValue and MaxValue included
NextByte(min, max), NextSByte(min, max), NextInt16(min, max), NextInt32, NextUInt16(min, max), NextUInt32(min, max), NextUInt64(min, max)An integer, from min (included) to max (excluded)
NextSingle, NextDouble, NextDecimalA number from min (included) to max (excluded)
NextDateTime(min, max)A date from min (included) to max (excluded)
NextFromList(list)A random element of an array, a list, a collection, a span or a memory
NextString(length, chars)A string of length characters taken from chars
NextString(minLength, maxLength, chars)The same, with a length from minLength to maxLength, both included
  • To get any value of the type, call the overload without parameters: the maximum of a range is excluded, so NextByte(0, 255) never returns 255.
  • NextInt32 works like Random.Next: min and max default to 0 and int.MaxValue, so it returns a non-negative value by default.
  • For long, use Random.NextInt64. RandomExtensions.NextInt64 is obsolete: random.NextInt64(...) always calls the method of Random.
  • NextSingle, NextDouble and NextDecimal default to 0 and 1, or to the whole range of decimal. They work for any range, including double.MinValue to double.MaxValue. They throw an ArgumentOutOfRangeException when min is greater than max, as does NextDateTime. random.NextSingle() calls Random.NextSingle, which also returns a value from 0 to 1.
  • NextDecimal scales the result of Random.NextDouble, so it cannot return every value of a large range. NextUInt64 uses the 64 bits of a random value.
  • NextFromList accepts a List<T>, a HashSet<T> or any other collection. For a collection that is not a list, it enumerates the collection to find the element.
  • These methods are not suited for security. Use System.Security.Cryptography.RandomNumberGenerator for passwords, tokens or keys.