rm.Extensions 3.0.2

This package has a SemVer 2.0.0 package version: 3.0.2+0f900fb3b4c4ffad1ab0f295520a137cd2e61dfd.
There is a newer version of this package available.
See the version list below for details.
dotnet add package rm.Extensions --version 3.0.2                
NuGet\Install-Package rm.Extensions -Version 3.0.2                
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.0.2" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add rm.Extensions --version 3.0.2                
#r "nuget: rm.Extensions, 3.0.2"                
#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.0.2

// Install rm.Extensions as a Cake Tool
#tool nuget:?package=rm.Extensions&version=3.0.2                

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
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 2,985 11/19/2023
3.1.0 195 10/15/2023
3.1.0-alpha3 50 10/14/2023
3.1.0-alpha2 65 9/24/2023
3.1.0-alpha1 78 3/12/2023
3.1.0-alpha0 80 2/12/2023
3.0.2 161 9/24/2023
3.0.1 2,863 2/12/2023
3.0.0-alpha0 95 2/2/2023
2.10.1-alpha 91 1/29/2023
2.10.0-alpha1 89 1/3/2023
2.9.2-alpha 91 1/29/2023
2.9.1-alpha1 98 8/14/2022
2.9.0-alpha0 124 1/2/2022
2.8.2 13,821 1/29/2023
2.8.2-alpha 92 1/29/2023
2.8.1 2,688 2/14/2022
2.8.1-alpha0 99 2/14/2022
2.8.0 1,260 1/2/2022
2.8.0-alpha0 122 1/2/2022
2.7.1 4,954 11/28/2021
2.7.1-alpha0 1,273 11/28/2021
2.7.0 163 11/14/2021
2.7.0-alpha0 149 11/14/2021
2.6.0 177 11/14/2021
2.6.0-alpha0 259 11/14/2021
2.5.4 1,728 11/2/2021
2.5.4-alpha0 146 11/2/2021
2.5.3-alpha0 163 11/2/2021
2.5.2 202 11/1/2021
2.5.2-alpha0 201 11/1/2021
2.5.1 222 10/31/2021
2.5.0 214 10/31/2021
2.5.0-alpha1 206 10/31/2021
2.5.0-alpha0 129 10/22/2021
2.4.0 213 10/30/2021
2.4.0-alpha2 214 10/30/2021
2.4.0-alpha1 208 10/2/2021
2.4.0-alpha0 134 9/23/2021
2.3.0 271 10/22/2021
2.3.0-alpha1 152 10/22/2021
2.3.0-alpha0 147 9/23/2021
2.2.0 202 9/12/2021
2.2.0-alpha1 185 9/9/2021
2.2.0-alpha0 174 9/9/2021
2.1.0 184 9/6/2021
2.1.0-alpha3 133 9/6/2021
2.1.0-alpha2 137 9/6/2021
2.1.0-alpha1 195 9/5/2021
2.1.0-alpha0 136 9/5/2021
2.0.0 157 9/6/2021
2.0.0-alpha2 134 8/24/2021
2.0.0-alpha1 141 8/23/2021
2.0.0-alpha 340 11/28/2020
1.5.1-alpha 241 11/3/2020
1.5.0 404 9/5/2021
1.5.0-alpha 406 9/13/2020
1.4.0 990 8/24/2020
1.4.0-alpha 416 8/23/2020
1.3.6-alpha 465 5/20/2019
1.3.5-alpha 526 1/8/2019
1.3.4-alpha 552 12/23/2018
1.3.1-alpha 524 12/22/2018
1.3.0-alpha 610 12/3/2018
1.2.2 793 12/3/2018
1.2.2-alpha 657 7/29/2018
1.2.1-alpha 694 3/6/2018
1.1.2-alpha 797 3/4/2018
1.1.1-alpha 706 2/17/2018
1.1.0-alpha 765 2/17/2018
1.0.0-alpha 965 2/11/2018

tag: v3.0.2