rm.Extensions 3.1.0-alpha2

This is a prerelease version of rm.Extensions.
This package has a SemVer 2.0.0 package version: 3.1.0-alpha2+61424d32de980187a4363de5b5052c5164963bf6.
There is a newer version of this package available.
See the version list below for details.
dotnet add package rm.Extensions --version 3.1.0-alpha2
NuGet\Install-Package rm.Extensions -Version 3.1.0-alpha2
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="rm.Extensions" Version="3.1.0-alpha2" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add rm.Extensions --version 3.1.0-alpha2
#r "nuget: rm.Extensions, 3.1.0-alpha2"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install rm.Extensions as a Cake Addin
#addin nuget:?package=rm.Extensions&version=3.1.0-alpha2&prerelease

// Install rm.Extensions as a Cake Tool
#tool nuget:?package=rm.Extensions&version=3.1.0-alpha2&prerelease

csharp-extensions

A collection of utility C# extension methods.

main: Build Status dev: Build Status

nuget:
Install-Package rm.Extensions

NuGet version (rm.Extensions)

string extensions:
var s = "";
if (s.IsNullOrEmpty()) { /**/ }
if (s.IsNullOrWhiteSpace()) { /**/ }
// some string that could be null/empty/whitespace
string s = null; // or "value"
string text = "default";
if (!s.IsNullOrWhiteSpace()) text = s.Trim();
// fluent code by avoiding comparison
string text = s.OrEmpty().Trim(); // "" when s is null/empty/whitespace
string text = s.Or("default").Trim(); // "default" when s is null/empty/whitespace
// or using null-conditional and null-coalesce operators
string text = s.NullIfEmpty()?.Trim() ?? ""
string text = s.NullIfWhiteSpace()?.Trim() ?? "default"
// html-en/decode, url-en/decode
string s = "";
string htmlencoded = s.HtmlEncode();
string htmldecoded = s.HtmlDecode();
string urlencoded = s.UrlEncode();
string urldecoded = s.UrlDecode();
// "".Format() instead of string.Format()
"{0} is a {1}".Format("this", "test");
// parameter index is optional
"{} is a {}".Format("this", "test");
"{} is a {1}".Format("this", "test"); // mixing is ok
// parameter meta is allowed
"The name is {0}. {first} {last}.".Format(lastName, firstName, lastName); // adding arg meta is ok
"The name is {last}. {first} {last}.".Format(lastName, firstName); // bit intelligent about repeating arg meta
// bool try-parse string with default value
bool b = "".ToBool(defaultValue: true);
// b: true
// munge a password, up to two chars
string[] munged = "pass".Munge().ToArray();
// munged: { "pa$$", "pa55", "p@ss", "p@$$", "p@55" }
string[] munged = "ai".Munge().ToArray();
// munged: { "a1", "a!", "@i", "@1", "@!" }
string[] munged = "pw".Munge().ToArray();
// munged: { "puu", "p2u" }
// unmunge a password
string[] unmunged = "h@x0r".Unmunge().ToArray();
// unmunged: { "haxor" }
string[] unmunged = "puu".Unmunge().ToArray();
string[] unmunged = "p2u".Unmunge().ToArray();
// unmunged: { "pw" }
// scrabble characters of word (like the game)
var word = "on";
var scrabbled = word.Scrabble();
// scrabbled: { "o", "on", "n", "no" }
// parse a string in UTC format as DateTime
DateTime date = "2013-04-01T03:42:14-04:00".ParseAsUtc();
// date: 4/1/2013 7:42:14 AM, Kind: Utc
// convert a string to title case
string result = "war and peace".ToTitleCase();
// result: "War And Peace"
// split a csv string
string[] result = "a,b;c|d".SplitCsv().ToArray();
// result: [ "a", "b", "c", "d" ]
// substring from start
string result = "this is a test".SubstringFromStart(4);
// result: "this"
// substring till end
string result = "this is a test".SubstringTillEnd(4);
// result: "test"
// substring by specifying start index and end index
string result = "this".SubstringByIndex(1, 3);
// result: "hi"
ThrowIf extensions:
public void SomeMethod(object obj1, object obj2) 
{
	// throws ArgumentNullException if object is null
	obj1.ThrowIfArgumentNull("obj1");
	obj2.ThrowIfArgumentNull("obj2");
	// OR 
	new[] { obj1, obj2 }.ThrowIfAnyArgumentNull();
	
	// ...
	
	object obj = DoSomething();
	// throws NullReferenceException if object is null
	obj.ThrowIfNull("obj");
	// OR 
	new[] { obj1, obj2 }.ThrowIfAnyNull();
}
public void SomeMethod(string s1, string s2) 
{
	// throws ArgumentNullException or EmptyException if string is null or empty
	s1.ThrowIfNullOrEmptyArgument("s1"); // or s1.ThrowIfNullOrWhiteSpaceArgument("s1")
	s2.ThrowIfNullOrEmptyArgument("s2");
	// OR 
	new[] { s1, s2 }.ThrowIfNullOrEmptyArgument();
	
	// ...
	
	string s = DoSomething();
	// throws NullReferenceException or EmptyException if string is null or empty.
	s.ThrowIfNullOrEmpty("s"); // or s1.ThrowIfNullOrWhiteSpace("s")
}
DateTime extensions:
// gives date in UTC format string
string dateUtc = date.ToUtcFormatString();
// dateUtc: "1994-11-05T13:15:30.000Z"
// gives min date that can be inserted in sql database without exception (SqlDateTime.MinValue)
DateTime date = new DateTime().ToSqlDateTimeMinUtc();
// date: 1/1/1753 12:00:00 AM
// date read from db or parsed from string has its Kind as Unspecified.
// specifying its kind as UTC is needed if date is expected to be UTC.
// ToUniversalTime() assumes that the kind is local while converting it and is undesirable.
DateTime date = DateTime.Parse("4/1/2014 12:00:00 AM").AsUtcKind();
// date: 4/1/2014 12:00:00 AM, Kind: Utc
IEnumerable extensions:
// creates chunks of given collection of specified size
IEnumerable<IEnumerable<int>> chunks = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }.Chunk(3);
// chunks: { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 }, { 10 } }
// if a collection is null or empty
var collection = new[] { 1, 2 };
if (collection.IsNullOrEmpty()) { /**/ }
// split a collection into n parts
var collection = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
IEnumerable<IEnumerable<int>> splits = collection.Split(3);
// splits: { { 1, 4, 7, 10 }, { 2, 5, 8 }, { 3, 6, 9 } }
// if a collection is sorted
var collection1 = new[] { 1, 2, 3, 4 };
bool isSorted1 = collection1.IsSorted();
var collection2 = new[] { 7, 5, 3 };
bool isSorted2 = collection2.IsSorted();
// isSorted1, isSorted2: true
// Double(), DoubleOrDefault() as Single(), SingleOrDefault()
IEnumerable<int> doubleitems = new[] { 1, 2 }.Double();
// doubleitems: { 1, 2 }
IEnumerable<int> doubleitems = new[] { 1, 2, 3 }.Double(x => x > 1);
// doubleitems: { 2, 3 }
IEnumerable<int> doubleordefaultitems = new int[0].DoubleOrDefault();
// doubleordefaultitems: null
IEnumerable<int> doubleordefaultitems = new[] { 1, 2, 3 }.DoubleOrDefault(x => x > 1);
// doubleordefaultitems: { 2, 3 }

// throws InvalidOperationException
new[] { 1 }.Double();
new[] { 1 }.Double(x => x > 0);
new[] { 1 }.DoubleOrDefault();
new[] { 1 }.DoubleOrDefault(x => x > 0);
// shuffle collection in O(n) time (Fisher-Yates shuffle, revised by Knuth)
var shuffled = new[] { 0, 1, 2, 3 }.Shuffle();
// shuffled: { 2, 3, 1, 0 }
// slice a collection as Python (http://docs.python.org/2/tutorial/introduction.html#strings)
var a = new[] { 0, 1, 2, 3, 4 }
var slice = a.Slice(step: 2);
// slice: { 0, 2, 4 }

a.Slice(start, end);		// items start through end-1
a.Slice(start);				// items start through the rest of the array
a.Slice(0, end);			// items from the beginning through end-1
a.Slice();					// a copy of the whole array
a.Slice(start, end, step);	// start through not past end, by step
a.Slice(-1);				// last item in the array
a.Slice(-2);				// last two items in the array
a.Slice(-3, -2);			// third last item in the array
a.Slice(0, -2);				// everything except the last two items
a.Slice(step: -1);			// copy with array reversed

// help
a.Slice(end: 2)			// 1st 2
a.Slice(2)				// except 1st 2
a.Slice(-2)				// last 2
a.Slice(0, -2)			// except last 2
a.Slice(1, 1 + 1)		// 2nd char
a.Slice(-2, -2 + 1)		// 2nd last char
// scrabble a list of words (like the game)
var words = { "this", "test" };
var scrabbled = words.Scrabble();
// scrabbled: { "this", "thistest", "test", "testthis" }
// check an enumerable's count efficiently
if (enumerable.Count() == 2) { ... } // inefficient for large enumerable
if (enumerable.HasCount(2)) { ... }
if (enumerable.HasCountOfAtLeast(2)) { ... } // count >= 2
// get permutations or combinations for particular r
var result = new[] { 1, 2 }.Permutation(2);
// result: { { 1, 2 }, { 2, 1 } }
var result = new[] { 1, 2 }.Combination(2);
// result: { { 1, 2 } }
// if a collection is empty instead of !collection.Any()
var collection = new[] { 1, 2 };
if (collection.IsEmpty()) { /**/ }
// get top n or bottom n efficiently (using min/max-heap)
IEnumerable<int> top_n = { 2, 3, 1, 4, 5 }.Top(3);
IEnumerable<int> bottom_n = { 2, 3, 1, 4, 5 }.Bottom(3);
// top_n: { 3, 5, 4 }
// bottom_n: { 3, 1, 2 }
// get top n or bottom n from IEnumerable
IEnumerable<Person> top_n = persons.Top(3);
IEnumerable<Person> bottom_n = persons.Bottom(3);
// get top n or bottom n by using a key selector or/and comparer
IEnumerable<Person> oldest_3 = persons.Top(3, x => x.Age);
IEnumerable<Person> youngest_3 = persons.Bottom(3, x => x.Age);
IEnumerable<Person> oldest_3 = persons.Top(3, personByAgeComparer);
IEnumerable<Person> youngest_3 = persons.Bottom(3, personByAgeComparer);
IEnumerable<Person> oldest_3 = persons.Top(3, x => x.Age, ageComparer);
IEnumerable<Person> youngest_3 = persons.Bottom(3, x => x.Age, ageComparer);
// source.Except(second, comparer) linqified instead of a full-blown class for comparer
source.ExceptBy(second, x => x.Member);
// same for source.Distinct(comparer)
source.DistinctBy(x => x.Member);
// same for source.OrderBy(keySelector, comparer)
source.OrderBy(x => x.Property,
	(p1, p2) => p1.CompareTo(p2) // where p1, p2 are of same type as x.Property
);
// source.OrEmpty() or source.EmptyIfDefault() to avoid a null check
foreach (var item in source.OrEmpty()) { /**/ }
foreach (var item in source.EmptyIfDefault()) { /**/ }
// instead of
if (source != null) { foreach (var item in source) { /**/ } }
// TrySingle() to get single without exception
if (source.TrySingle(out singleT)) { ... }
// OneOrDefault() to get the only one element or default
//	input		firstOrDefault	singleOrDefault		oneOrDefault
//	{ 1, 2 }	1				throws				default
//	{ 1 }		1				1					1
//	{ }			default			default				default
var oneT = source.OneOrDefault();
// calls IEnumerable.Contains()
bool result = value.In(source);
bool result = value.In(source, comparer);
IList extensions:
// RemoveLast() to remove last item(s) in list
list.RemoveLast();
list.RemoveLast(2);
Enum extensions:
enum Color { Red = 1, Green, Blue };
Color color = "Red".Parse<Color>();
// OR
Color color;
"Red".TryParse<Color>(out color);
// color: Color.Red
enum Color
{
	[Description("Red color")] Red = 1, 
	Green, 
	[Description("Blue color")] Blue
}
string redDesc = Color.Red.GetDescription();
string greenDesc = Color.Green.GetDescription();
// redDesc: "Red color"
// greenDesc: "Green"
enum Color { Red = 1, Green, Blue };
Color color = "Red".GetEnumValue<Color>();
// color: Color.Red

// enumValue.GetEnumName() is fastest of all
// fastest, dictionary lookup after 1st call
if (Color.Red.GetEnumName() == "Red") { /**/ }
// slightly slow, dictionary lookup after 1st call
if ("Red".GetEnumValue<Color>() == Color.Red) { /**/ }
// slow, due to reflection
if ("Red".Parse<Color>() == Color.Red) { /**/ }
// slowest, due to reflection
if (Color.Red.ToString() == "Red") { /**/ }
enum Color
{
	[Description("Red color")] Red = 1, 
	Green, 
	[Description("Blue color")] Blue
}
IDictionary<string, string> colorsMap = EnumExtension.GetEnumNameToDescriptionMap<Color>();
// build a select list
IEnumerable<ListItem> selectOptions = colorsMap
	.Select(x => new ListItem() { text: x.Value, value: x.Key });
//  <select>
//    <option value="Red">Red color</option>
//    <option value="Green">Green</option>
//    <option value="Blue">Blue color</option>
//  </select>
enum Color
{
	[Description("Red color")] Red = 1, 
	Green, 
	[Description("Blue color")] Blue
}
string redName = "Red color".GetEnumNameFromDescription<Color>()
// redName: "Red"
bool hasValue = 0.IsDefined<Color>();
// hasValue: false
bool hasValue = 1.IsDefined<Color>();
// hasValue: true
enum Color
{
	[Description("Red color")] Red = 1, 
	Green, 
	[Description("Blue color")] Blue
}
// compile error: cannot have int as enum values or hyphen sign in enum values
enum Grade { Toddler, Pre-K, Kindergarten, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, College }
// work-around: use Description attribute
enum Grade 
{
	Toddler = 1, 
	[Description("Pre-K")] PreK, 
	Kindergarten, 
	[Description("1")] One, 
	[Description("2")] Two, 
	[Description("3")] Three, 
	[Description("4")] Four, 
	[Description("5")] Five, 
	[Description("6")] Six, 
	[Description("7")] Seven, 
	[Description("8")] Eight, 
	[Description("9")] Nine, 
	[Description("10")] Ten, 
	[Description("11")] Eleven, 
	[Description("12")] Twelve, 
	College
}

// to sort gradesUnsorted, use GetEnumValueFromDescription<T>() and GetDescription<T>() methods
string[] gradesUnsorted = new[] { "Pre-K", "1", "College", "2", "Toddler" };
Grade[] grades = gradesUnsorted
	.Select(x => x.GetEnumValueFromDescription<Grade>()).ToArray();
Array.Sort(grades);
string[] gradesSorted = grades.Select(x => x.GetDescription());
// gradesSorted: { "Toddler", "Pre-K", "1", "2", "College" } 
NameValueCollection extensions:
// get query string for name-value collection
NameValueCollection nvc = new NameValueCollection { {"k1,", "v1"}, {"k2", "v2"} };
string query = nvc.ToQueryString(); // OR nvc.ToQueryString(prefixQuestionMark: false);
// query: "?k1%2C=v1&k2=v2" // OR "k1%2C=v1&k2=v2"
TimeSpan extensions:
// round timespan as ms, s, m, h, d, wk, mth, y.
string round = TimeSpan.FromDays(10).Round();
// round: "1wk"
// n Days, Hours, Minutes, Seconds, Milliseconds, etc.
TimeSpan ts = 10.Days();
Uri extensions:
// calculate uri's checksum (sha1, md5)
var uri = new Uri(@"https://www.google.com/images/srpr/logo11w.png") // url
	// OR new Uri(@"D:\temp\images\logo.png"); // local file
	// OR new Uri(new Uri(@"D:\temp\"), @".\images\logo.png"); // dir and relative path
string sha1 = uri.Checksum(Hasher.sha1);
string md5 = uri.Checksum(Hasher.md5);
// sha1: 349841408d1aa1f5a8892686fbdf54777afc0b2c
// md5: 57e396baedfe1a034590339082b9abce
Helper methods:
// swap two values or references
Helper.Swap(ref a, ref b);
decimal extensions:
// truncate decimal to specified digits
12.349m.TruncateTo(2); // 12.34m
int extensions:
// round int as k, m, g
1000.Round(); // "1k"
1000000.Round(); // "1m"
1500.Round(); // "1k"
1500.Round(1); // "1.5k"
Graph extensions:
// if graph is cyclic (used for deadlock detection)
bool isCyclic = graph.IsCyclic();
StringBuilder extensions:
// instead of buffer.AppendLine(string.Format(format, args))
buffer.AppendLine(format, args);
// reverse StringBuilder in-place
buffer.Reverse();
Dictionary extensions:
// for key in dictionary, get value if exists or default / specified value
var value = dictionary.GetValueOrDefault(key);
var value = dictionary.GetValueOrDefault(key, other);
// get dictionary as readonly
var dictionaryReadonly = dictionary.AsReadOnly();
Wrapped extensions:
// wrap (box) any type to avoid using pass by ref parameters
var intw = new Wrapped<int>(1); // or 1.Wrap();
// intw.Value = 1
BitSet:
BitSet bitset = new BitSet(10); // 0 to 10 inclusive
bitset.Add(5); // add 5
bitset.Add(6);
bitset.Remove(5); // remove 5
bitset.Remove(3);
bitset.Toggle(3); // toggle 3
bool has2 = bitset.Has(2); // if has 2
bitset.Clear(); // remove all
foreach(int item in bitset) { /**/ }
Circular Queue:
CircularQueue<int> cq = new CircularQueue<int>(capacity: 2);
cq.Enqueue(1);
cq.Enqueue(2);
cq.Enqueue(3);
cq.Enqueue(4);
int head;
head = cq.Dequeue(); // returns 3
head = cq.Dequeue(); // returns 4
Circular Stack:
CircularStack<int> cq = new CircularStack<int>(capacity: 2);
cq.Push(1);
cq.Push(2);
cq.Push(3);
cq.Push(4);
int top;
top = cq.Pop(); // returns 4
top = cq.Pop(); // returns 3
Deque:
Deque<int> dq = new Deque<int>();
Node<int> node = dq.Enqueue(1);
dq.Enqueue(2);
dq.Delete(node); // delete in O(1) time
int i = dq.Dequeue(); // returns 2
LRU cache:
LruCache<int, int> cache = new LruCache<int, int>(5);
cache.Insert(key: 2, value: 2);
cache.Get(2); // returns value 2
var count = cache.Count(); // return 1
cache.IsEmpty(); // returns false
cache.Remove(2); // removes 2 from cache
cache.IsFull(); // returns false
cache.Capacity(); // returns 5
cache.Clear(); // clears cache
Guid:
// guid.ToByteArray() is sensitive to endianness, but
// guid.ToByteArrayMatchingStringRepresentation() is not and matches guid.ToString()
// see https://stackoverflow.com/questions/9195551/why-does-guid-tobytearray-order-the-bytes-the-way-it-does
var bytes = Guid.Parse(someGuid).ToByteArrayMatchingStringRepresentation();
// similar roundtrip method
var guid = bytes.ToGuidMatchingStringRepresentation(); // same as someGuid
Base64, Base64Url:
var base64 = s.ToUtf8Bytes().Base64Encode();
var s = base64.Base64Decode().ToUtf8String();

var base64Url = s.ToUtf8Bytes().Base64UrlEncode();
var s = base64Url.Base64UrlDecode().ToUtf8String();
Random:
var gaussian = rng.NextGaussian(mu: mu, sigma: sigma);
var s = rng.NextString(length: 10, charset: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_");
var doubleN = rng.NextDouble(minValue: 5.0d, maxValue: 10.0d);
var decimalN = rng.NextDecimal(minValue: 5.0m, maxValue: 10.0m);
Base16 (Hex):

note: net5.0+ has Convert.ToHexString(bytes), Convert.FromHexString(hex) which are faster than below.

var base16 = s.ToUtf8Bytes().Base16Encode();
var s = base16.Base16Decode().ToUtf8String();
// or
var hex = s.ToUtf8Bytes().ToHexString();
var s = hex.FromHexString().ToUtf8String();
char extensions:
// all/most System.Char static utility functions
var c = 'a'.ToUpper(); // returns 'A'
var isLower = c.IsLower(); // returns false
var isLetter = c.IsLetter(); // returns true
Base32:
// Douglas Crockford's Base32 impl
var base32 = s.ToUtf8Bytes().Base32Encode();
var s = base32.Base32Decode().ToUtf8String();
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 is compatible. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.
  • .NETStandard 2.1

    • No dependencies.
  • net6.0

    • No dependencies.
  • net7.0

    • No dependencies.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on rm.Extensions:

Package Downloads
rm.DelegatingHandlers

Provides DelegatingHandlers.

rm.Masking

Provides masking methods.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
3.1.1 763 11/19/2023
3.1.0 177 10/15/2023
3.1.0-alpha3 35 10/14/2023
3.1.0-alpha2 52 9/24/2023
3.1.0-alpha1 65 3/12/2023
3.1.0-alpha0 67 2/12/2023
3.0.2 146 9/24/2023
3.0.1 1,862 2/12/2023
3.0.0-alpha0 81 2/2/2023
2.10.1-alpha 78 1/29/2023
2.10.0-alpha1 76 1/3/2023
2.9.2-alpha 77 1/29/2023
2.9.1-alpha1 84 8/14/2022
2.9.0-alpha0 112 1/2/2022
2.8.2 11,374 1/29/2023
2.8.2-alpha 78 1/29/2023
2.8.1 2,526 2/14/2022
2.8.1-alpha0 87 2/14/2022
2.8.0 887 1/2/2022
2.8.0-alpha0 110 1/2/2022
2.7.1 2,184 11/28/2021
2.7.1-alpha0 1,259 11/28/2021
2.7.0 149 11/14/2021
2.7.0-alpha0 136 11/14/2021
2.6.0 162 11/14/2021
2.6.0-alpha0 245 11/14/2021
2.5.4 1,034 11/2/2021
2.5.4-alpha0 132 11/2/2021
2.5.3-alpha0 150 11/2/2021
2.5.2 187 11/1/2021
2.5.2-alpha0 182 11/1/2021
2.5.1 206 10/31/2021
2.5.0 200 10/31/2021
2.5.0-alpha1 194 10/31/2021
2.5.0-alpha0 116 10/22/2021
2.4.0 198 10/30/2021
2.4.0-alpha2 200 10/30/2021
2.4.0-alpha1 195 10/2/2021
2.4.0-alpha0 121 9/23/2021
2.3.0 254 10/22/2021
2.3.0-alpha1 132 10/22/2021
2.3.0-alpha0 135 9/23/2021
2.2.0 186 9/12/2021
2.2.0-alpha1 167 9/9/2021
2.2.0-alpha0 159 9/9/2021
2.1.0 169 9/6/2021
2.1.0-alpha3 121 9/6/2021
2.1.0-alpha2 124 9/6/2021
2.1.0-alpha1 183 9/5/2021
2.1.0-alpha0 117 9/5/2021
2.0.0 141 9/6/2021
2.0.0-alpha2 121 8/24/2021
2.0.0-alpha1 129 8/23/2021
2.0.0-alpha 327 11/28/2020
1.5.1-alpha 221 11/3/2020
1.5.0 372 9/5/2021
1.5.0-alpha 377 9/13/2020
1.4.0 948 8/24/2020
1.4.0-alpha 388 8/23/2020
1.3.6-alpha 437 5/20/2019
1.3.5-alpha 502 1/8/2019
1.3.4-alpha 528 12/23/2018
1.3.1-alpha 500 12/22/2018
1.3.0-alpha 583 12/3/2018
1.2.2 759 12/3/2018
1.2.2-alpha 632 7/29/2018
1.2.1-alpha 667 3/6/2018
1.1.2-alpha 766 3/4/2018
1.1.1-alpha 681 2/17/2018
1.1.0-alpha 739 2/17/2018
1.0.0-alpha 936 2/11/2018

tag: v3.1.0-alpha2