Dates, numbers and ranges
editStart 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());| Method | Returns |
|---|---|
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
.Dateon 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
Kindof the date (Utc,LocalorUnspecified). - 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)));| Method | Lower bound | Upper bound |
|---|---|---|
| IsInRangeInclusive | Included | Included |
| IsInRangeExclusive | Excluded | Excluded |
| IsInRangeLowerInclusive | Included | Excluded |
| IsInRangeUpperInclusive | Excluded | Included |
- Each method has an overload that takes a Range<T>: it returns
truewhen both bounds of that range pass the check. - For a reference type, such as
string, anullbound is not checked: the range is open on that side. - The constructor throws an
ArgumentExceptionwhen 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);| Method | Returns |
|---|---|
| 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, NextDecimal | A 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:minandmaxdefault to 0 andint.MaxValue, so it returns a non-negative value by default. - For
long, useRandom.NextInt64.RandomExtensions.NextInt64is obsolete:random.NextInt64(...)always calls the method ofRandom. - NextSingle, NextDouble and NextDecimal default to 0 and 1, or to the whole range of
decimal. They work for any range, includingdouble.MinValuetodouble.MaxValue. They throw anArgumentOutOfRangeExceptionwhenminis greater thanmax, as doesNextDateTime.random.NextSingle()callsRandom.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. NextFromListaccepts aList<T>, aHashSet<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.RandomNumberGeneratorfor passwords, tokens or keys.