Skip to content

icarus-consulting/Yaapii.Atoms

Repository files navigation

Build status codecov PRs Welcome Commitizen friendly EO principles respected here

Overview

Object-Oriented Primitives for .NET. This is a .NET port of the java library Cactoos by Yegor Bugayenko.

It follows all the rules suggested in the two "Elegant Objects" books.

Maintainer[s]:

Table Of Contents

Design change Version 1.0 vs 2.0

Caching

Version 1 of Atoms follows the principles of cactoos. All objects in cactoos are so-called live objects. This means, if you create a Text from a url new TextOf(new Uri("http://www.google.de")), every call to that object will fetch the content again. There is no caching until you explicitely define it by using a Sticky object. Sticky objects exist for all Types (Text, Enumerable, Map...).

However, after two years of working with Atoms, we realized that developers in our teams tend to think differently. They build objects and use them as if they have been cached. This produces a lot of unnecessary calculations and leads to slowdowns in our apps which we then solve in a second round where we analyze which objects should have been sticky. On the other hand, there are only a few cases where developers really need the live sensing of changes.

This has led to the decision to invert the library caching principle. Atoms 2.0 now has all objects sticky by default. We then introduced new Live Decorators instead. So if you need an object which senses changes, you decorate it using the live decorator:

var exchangeRate = new Live(() => new TextOf(new Uri("https://api.exchangeratesapi.io/latest")));

Live decorators are available for all types.

Caching envelopes

If you want to write your own objects based on Atoms envelopes, you have a switch which you can use to tell the envelope if it should behave as a live object or not (default is no)

public sealed class MyLiveTextObject : TextEnvelope
{
     MyLiveTextObject(...) : base(..., live: true)
}

Exception: Input and output

Input and Output types are not sticky by default.

Shorthand Generics

While Java developers can skip the generic expression in constructors, C# does not allow it. We added shorthand objects to allow skipping it if you use string as generic type, for Enumerables and Maps. You have two objects to make an Enumerable: new ManyOf to get an enumerable of strings and new ManyOf<T> if you need another type.

There are three objects to make a map:

new MapOf(new KvpOf("Key", "Value")); //A map string to string
new MapOf<int>(new KvpOf<int>("Key", 123)); //A map string to generic type
new MapOf<int, int>(new KvpOf<int, int>(123, 123)); //A map generic type to generic type

Envelopes are available for all three map types.

Migration

We decided to not leave the old sticky objects in the new version to get a fresh start.

If you want to migrate from 1.x to 2.x and want to preserve the behaviour regarding sticky objects, you should (in this order):

  • Replace ScalarOf with Live
  • Replace Sticky with ScalarOf
  • Replace ManyOf with LiveMany
  • Replace StickyEnumerable with ManyOf
  • Replace ListOf with LiveList
  • Replace StickyList with ListOf
  • Replace CollectionOf with LiveCollection
  • Replace Collection.Sticky with CollectionOf
  • Replace TextOf with LiveText (StickyText did not exist)
  • Replace NumberOf with LiveNumber (Stickynumber did not exist)

Maps

Maps are grouped in the Lookup Area of atoms. All map objects implement C# IDictionary<Key, Value> interface.

var map = new MapOf<string, int>("Amount", 100);

Kvp

CSharp maps use KeyValuePair<Key,Value> as contents. These are structs and therefore not objects which we can implement or decorate. Atoms offers the type IKVp<Key,Value> as alternative.

var map = 
   new MapOf<string,int>(
      new KvpOf<string, int>("age", 28),
      new KvpOf<string, int>("height", 184),
   )

Lazy values

This allows maps to accept functions to build the value, and get a simple strategy pattern

//If you ask the map for "github", it will execute the function and retrieve the content. The content of the first kvp is NOT retrieved.
var githubContent =
   new MapOf<string,string>(
      new KvpOf<string,string>("google", () => new TextOf(new Uri("https://www.google.de"))),
      new KvpOf<string,string>("github", () => new TextOf(new Uri("https://www.github.com")))
   )["github"];

//Note that MapOf is sticky, so if you need live content, decorate the map:
var liveContent =
   new LiveMap<string,string>(() =>
      new MapOf<string,string>(
         new KvpOf<string,string>("google", () => new TextOf(new Uri("https://www.google.de"))),
         new KvpOf<string,string>("github", () => new TextOf(new Uri("https://www.github.com")))
      )
   )["github"];

//Beware: If you have lazy values, you normally do NOT want to execute all functions. Atoms prevents it, so the following will fail:
foreach(var doNotDoThis in githubContent)
{
   ...
}

//If you know that you need all values, simply enumerate the keys:
foreach(var key in githubContent.Keys)
{
  var value = githubContent[key];
}

Shorthand Generics

To save typing, there are two shorthand map objects:

new MapOf(new KvpOf("Key", "Value")) //Use without generic to get a string-string map
new MapOf<int>(new KvpOf("Key", 100)) //Use without generic to get a string-generic map

Shorthand Stringmap

You can make a string-string map by simple writing value pairs one after another:

var translations = 
   new MapOf(
   	"Objects", "Objekte",
   	"Functions", "Funktionen",
   	"Bigmac", "Viertelpfünder mit Käse"
   )

Functions

The interfaces are:

//Function with input and output
public interface IFunc<In, Out>
{
    Out Invoke(In input);
}

//Function with output only
public interface IFunc<Out>
{
    Out Invoke();
}

//Function with two inputs and one output
public interface IFunc<In1, In2, Out>
{
    Out Invoke(In1 input1, In2 input2);
}

//Function with input only
public interface IAction<In>
{
    void Invoke(In input);
}

Execute functions

var i = new FuncOf<int, int>(
                (number) => number++
            ).Invoke(1); //i will be 2

Cache function output

var url = new Url("https://www.google.de");
var f = new StickyFunc<Url, IText>((u) =>
            new TextOf(
                new InputOf(u))
        ).Invoke(url);

var html = f.Invoke(); //will load the page content from the web
var html = f.Invoke(); //will load the page content from internal cache

Retry a function

new RetryFunc<int, int>(
    input =>
    {
        if (new Random().NextDouble() > input)
        {
            throw new ArgumentException("May happen");
        }
        return 0;
    },
    100000
).Invoke(0.3d); //will try 100.000 times to get a random number under 0.3

Repeat a function

var count = 0;
new RepeatedFunc<int, int>(
        input => input++,
        3
    ).Invoke(count); //will return 3

Use a fallback if a function fails

new FuncWithFallback<string, string>(
    name =>
    {
        throw new Exception("Failure");
    },
    ex => "Never mind, " + name
).Invoke("Miro"); //will be "Never mind, Miro"
  • And more

IO Input / Output

The IInput and IOutput interfaces:

public interface IInput
{
    Stream Stream();       
}

public interface IOutput
{
    Stream Stream();
}

Input Stream of a file

new InputOf(
        new Uri(@"file:///c:\secret-content-inside.txt")
    ).Stream(); //returns a readable stream of the file

Output Stream to a file

new OutputTo(
        new Uri(@"file:///c:\secret-content-inside.txt")
    ).Stream(); //returns a readable stream of the file

Output Stream of Console

new ConsoleOutput().Stream(); //Default console output
new ConsoleErrorOutput().Stream(); //Console error output

Read file content as string

var fileContent = 
    new TextOf(
        new InputOf(
                new Uri(@"file:///c:\secret-content-inside.txt")
            )
    ).AsString(); //reads the content and gives it as text

Read Url page as string

new TextOf(
    new InputOf(
        new Url("https://www.google.de"))
).AsString(); //gives the content html of the google start page

Read a file and use a fallback if fails

new TextOf(
    new InputWithFallback(
        new InputOf(
            new Uri(Path.GetFullPath("/this-file-does-not-exist.txt")) //file to read
        ),
        new InputOf(new TextOf("Alternative text!")) //fallback to use
    ).AsString(); //will be "Alternative Text!"

Get the length of a file

long length =
    new LengthOf(
        new InputOf(
                new Uri(@"file:///c:\great-content-inside.txt")
            )).Value(); //will be the number of bytes in the file
  • Write to file, url, byte arrays, streams

Copy all you are reading from a Stream to a output stream

new LengthOf(
    new TeeInput(
        "Welcome to the world of c:!", 
        new OutputTo(new Uri(@"file:///c:\greeting.txt")))
    ).Value();  //will write "Welcome to the world of c:!" to the file c:\greeting.txt. 
                //This happens because TeeInput puts every byte that is read to the specified output. 
                //When calling Value(), every byte is read to count the content.

Copy input/output 1:1 while reading or writing

var inPath = Path.GetFullPath(@"file:///c:\input-file.txt");
var outPath = Path.GetFullPath(@"file:///c:\output-file.txt");

new LengthOf(
    new TeeInput(
        new InputOf(new Uri(inPath)),
        new OutputTo(new Uri(outPath))
    )).Value(); //since LengthOf will read all the content when calling Value(), all that has been read will be copied to the output path.

//Alternative: Copy to Console output
new LengthOf(
    new TeeInput(
        new InputOf(new Uri(inPath)),
        new ConsoleOutput()
    )).Value(); //will dump the read content to output

Cache what you have read

    new TextOf(
        new StickyInput(
            new InputOf(new Url("http://www.google.de"))));

var html1 = input.AsString(); //will read the url from the web
var html2 = input.AsString(); //will return from the cache

Enumerables

Enumerables use the IEnumerable and IEnumerator interfaces from C#:

public interface IEnumerable<out T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

public interface IEnumerator<out T> : IEnumerator, IDisposable
{
    T Current { get; }
}

Base Objects

Naming of base objects differs. To save chars, shorthand names are used:

//use without generic and get an IEnumerable<string>
var strings = new ManyOf("a string", "another string"); 

//use with generic and get an IEnumerable<T>
var ints = new ManyOf<int>(98, 20); 

Filter

new Filtered<string>(
    new List<string>() { "A", "B", "C" },
    (input) => input != "B"); //will be a list with "A" and "C" inside

Get an item from an enumerable

new ItemAt<int>(
        new ManyOf<int>(1, 2, 3),
        2
    ).Value(); //will be 3 (Zero based)

//To get the first item simply do not specify a position:
new ItemAt<int>(
        new ManyOf<int>(1, 2, 3)
    ).Value(); //will be 1

//To get an item with a fallback if it isn't there:
String fallback = "fallback";
                new ItemAt<string>(
                    new ManyOf<string>(), //empty list,
                    12, //position 12 which does not exist
                    fallback
                ).Value(); //will be "fallback"

Sort lists

//Default sorting is forward
new Sorted<int>(
    new ManyOf<int>(3, 2, 10, 44, -6, 0)
); //items will be sorted to -6, 0, 2, 3, 10, 44

//Use another comparator for sorting
new Sorted<string>(
    IReverseComparer<string>.Default, //comparator is from C#.NET library
    new ManyOf<string>(
        "a", "c", "hello", "dude", "Friend"
    )
); //will be sorted to hello, Friend, dude, c, a

Count items in enumerables

var l = new LengthOf<int>(
            new ManyOf<int>(1, 2, 3, 4, 5)
        ).Value(); //will be 5

Map items in an enumerable to another type

IText greeting = 
    new ItemAt<IText>(
        new Mapped<String, IText>(
            new ManyOf<string>("hello", "world", "damn"),
            input => new UpperText(new TextOf(input)) //is applied to every item and will make a uppertext of it
            ),
        0
    ).Value(); //will be "HELLO"
// Mapping items of a list to another type using index of items
new Mapped<string,string>(
    new List<string>() {"One", "Two", Three"},
    (input, index) => $"{input}={index+1}");
// Returns a IEnumerable<string> with Content {"One=1", "Two=2", Three=3"}

Create cycling lists

//here is a list with 3 items and you call the 7th item. The cycled list will not fail but start over when it reaches the end.
new ItemAt<string>(
    new Cycled<string>( //make a cycled list of the enumerable with 3 items
        new ManyOf<string>(
            "one", "two", "three"
            )),
    7
    ).Value(); //will be "two"

Join lists together

new LengthOf(
    new Joined<string>(
        new ManyOf<string>("hello", "world", "Miro"),
        new ManyOf<string>("how", "are", "you"),
        new ManyOf<string>("what's", "up")
    )
).Value(); //will be 8

Limit lists

new SumOfInts(
    new HeadOf<int>(
        new ManyOf<int>(0, 1, 2, 3, 4),
        3
    )).Value(); //will be 3 (0 + 1 + 2)

Cache list contents

//this snippet has an endless list, which then is limited to the size. Every time someone calls the list, size increases and the list would grow. But StickyEnumerable prevents that and always returns the same list.
int size = 2;
var list =
   new StickyEnumerable<int>(
       new HeadOf<int>(
           new Endless<int>(1),
           new ScalarOf<int>(() => Interlocked.Increment(ref size))
           ));

new LengthOf(list).Value(); //will be 2
new LengthOf(list).Value(); //will be 2
  • and more

Scalar

The IScalar interface looks like this:

public interface IScalar<T>
{
    T Value();
}

A scalar is an object which can encapsulate objects and functions that return objects. It enables you to let a function appear as its return value. This is very useful to keep constructors code-free but also keep your overall object count low.

Also, scalars can be used to perform logical operations like And, Or, Not and more on function results or objects.

Encapsulate objects

var sc1 = new ScalarOf<string>("this brave string");
string str = sc.Value(); //returns the string

var sc2 = new ScalarOf<IEnumerable<int>>(
            new ManyOf<int>(1,2,3,4,5));
IEnumerable<int> lst = sc2.Value(); //returns the list

Encapsulate functions

var sc =
    new ScalarOf<string>(
        () =>
        new TextOf(
            new InputOf(
                new Url("http://www.ars-technica.com")
            )
        ).AsString());

string html = sc.Value(); //will fetch the html from the url and return it as a string

Cache function results

var sc =
   new StickyScalar<string>(
       () =>
       new TextOf(
           new InputOf(
               new Url("http://www.ars-technica.com")
           )
       ).AsString()).Value();

string html = sc.Value(); //will fetch the html from the url and return it as a string
string html2 = sc.Value(); //will return the html from the cache

Logical And

var result = 
    new And<True>(
        () => true,
        () => false,
        () => true).Value(); //will be false

var number = 3;
new And<True>(
    () => true, //function that returns true
    () => number == 4 //function that returns false
).Value(); //will be false

//you can also pass scalars into AND, and more.

Logical ternary

new Ternary<bool, int>(
    new True(), //condition is true
    6, //if true
    16 //if false
).Value(); //will be 6

new Ternary<int, int>(
    5, //input to test
    input => input > 3, //condition
    input => input = 8, //return if condition true
    input => input = 2 //return if condition false
).Value(); //will be 8

And more...

  • Negative
  • Max
  • Min
  • Or

Text

The IText interface looks like this:

public interface IText : IEquatable<IText>
{
    String AsString();
}

Transform text

//Lower a text
new LowerText(
    new TextOf("HelLo!")).AsString(); //will be "hello!"

//upper a text
new UpperText(
    new TextOf("Hello!")).AsString(); //will be "HELLO!"

Reverse text

new ReversedText(
    new TextOf("Hello!")).AsString(); //"!olleH"

Trim text

new TrimmedText(
    new TextOf("  Hello!   \t ")
    ).AsString(); // "Hello!"

new TrimmedLeftText(
    new TextOf("  Hello!   \t ")
    ).AsString(); // "Hello!   \t "

new TrimmedRightText(
    new TextOf("  Hello!   \t ")
    ).AsString(); // "  Hello!"

Split text

IEnumerable<Text> splitted = 
    new SplitText(
        "Hello world!", "\\s+"
    );

Replace text

new ReplacedText(
    new TextOf("Hello!"),
    "ello",     // what to replace
    "i"         // replacement
).AsString();   // "Hi!"

Join texts

new JoinedText(
    " ", 
    "hello", 
    "world"
).AsString();// "hello world"

Format texts with arguments

new FormattedText(
        "{0} Formatted {1}", 1, "text"
    ).AsString(); // "1 Formatted text"

Convert from and to text

//text from a string with encoding
var content = "Greetings, Mr.Freeman!";
new TextOf(
    content,
    Encoding.UTF8
);

//text from a input with default encoding
var content = "Hello, my precious coding friend! with default charset";
new TextOf(
    new InputOf(content)
);

//text from a StringReader
String source = "Hello, Dude!";
new TextOf(
    new StringReader(source),
    Encoding.UTF8
);

//text from a char array
new TextOf(
    'O', ' ', 'q', 'u', 'e', ' ', 's', 'e', 'r', 'a',
    ' ', 'q', 'u', 'e', ' ', 's', 'e', 'r', 'a'
    );

//text from a byte array
byte[] bytes = new byte[] { (byte)0xCA, (byte)0xFE };
new TextOf(bytes);

//text from a StringBuilder
String starts = "Name it, ";
String ends = "then it exists!";
Assert.True(
        new TextOf(
            new StringBuilder(starts).Append(ends)
        );

//text from an exception
new TextOf(
    new IOException("It doesn't work at all")
);

LinQ Analogy

Standard Query Operators

LinQ Yaapii.Atoms
Aggregate Not available yet
All And<T>
Any Or<T>
AsEnumerable
var arr = new int[]{ 1, 2, 3, 4 }; 
var enumerable = new ManyOf<int>(arr);
Average
var avg = new AvgOf(1, 2, 3, 4).AsFloat(); //avg = 2.5
Cast Not available yet
Concat
var joined = new Joined<string>(
  new ManyOf<string>("dies", "ist"),
  new ManyOf<string>("ein", "Test")
).Value(); //joined = {"dies", "ist", "ein", "Test"}
Contains
var b = new Contains<string>(
  new ManyOf<string>("Hello", "my", "cat", "is", "missing"),
  (str) => str == "cat"
).Value()); //b = true
Count
var length = new LengthOf<int>(
  new ManyOf<int>(1, 2, 3, 4, 5)
).Value(); //length will be 5
DefaultIfEmpty Not available yet
Distinct
var dis = new Distinct<int>(
  new ManyOf<int>(1, 2, 3),
  new ManyOf<int>(10, 2, 30)
).Value() // dis = {1, 2, 3, 10, 30}
//actual with bug
ElementAt
var itm = new ItemAt<int>(
  new ManyOf<int>(0,2,5),
   2
).Value() //itm = 2
ElementAtOrDefault
var itm = new ItemAt<string>(
  new ManyOf<string>(),
  12,
  fallback
).Value() // itm = "fallback"
Empty
new EnmuerableOf<int>()
Except Not available yet
First
var list = new EnumerableO<int>(1, 2, 3);
 var first = new ItemAt<int>(list).Value();
 // first = 1
FirstOrDefault
var itm = new ItemAt<string>(
  new ManyOf<string>(),
  1,
  fallback
).Value() // itm = "fallback"
Foreach
var list = new List[];
new Each<int>(
  (i) => lst[i] = i,
  0,1,2
).Invoke(); //eachlist = {0,1,2}
GroupBy Not available yet
GroupJoin Not available yet
Intersect Not available yet
Join Not available yet
Last
var last = new ItemAt<int>(
  new Reversed<int>(
    new ManyOf(5, 6 ,7 ,8)
  )
).Value() // last = 8
LastOrDefault
var itm = new ItemAt<string>(
  new Reversed<string>(
    new ManyOf<string>()
  ),
  1,
  fallback
).Value() // itm = "fallback"
LongCount Not available yet*
Max
var max = new MaxOf(22, 2.5, 35.8).AsDouble(); //max = 35.8; .AsInt() = 35
Min
var max = new MaxOf(22, 2.5, 35.8).AsDouble(); //max = 2.5; .AsInt() = 2
OfType Not available yet
OrderBy
var sorted = new Sorted<int>(
  new ManyOf<int>(3, 2, 10, 44, -6, 0)
) //sorted = {-6, 0, 2, 3, 10, 44
OrderByDescending
var sorted = new Sorted<string>(
  IReverseComparer<string>.Default,
  new ManyOf<string>("a", "c", "hello", "dude", "Friend")
) //sorted = {hello, Friend, dude, c, a}
Range Not available yet
Repeat
var repeated = new Repeated<int>(10,5) // repeated = {10, 10, 10, 10, 10}
Reverse
 var reversed = new Reversed<int>(ManyOf(2,3,4)); //reversed = {4,3,2}
Select
var selected = Mapped<string,string>(
  new List<string>() {"One", "Two", Three"},
  (tStr, index) => $"{tStr}={index+1}"
)// selected = {"One=1", "Two=2", Three=3"}
SelectMany Not available yet
SequenceEqual Not available yet
Single Not available yet
SingleOrDefault Not available yet
Skip
var skipped = new Skipped<string>(
  new ManyOf<string>("one", "two", "three", "four"),
  2
) // skipped = {three, four}
SkipWhile Not available yet
Sum
var sum = new SumOf(
  1.5F, 2.5F, 3.5F
).AsFloat() //sum = 7.5
Take
var lmt = new HeadOf<int>(
  new ManyOf<int>(0, 1, 2, 3, 4),
  3
)//lmt = {0, 1, 2}
TakeWhile Not available yet
ThenBy Not available yet
ThenByDescending Not available yet
ToArray Not available yet
ToDictionary
var dic = new MapOf(
  new Enumerable<KeyValuePair<string,string>>(
    new KyValuePair<string,string>("key","value"),
    new KyValuePair<string,string>("key2", "value2")
  )
) // dic = {{key, value}{key2, value2}}
ToList
var list = new CollectionOf<int>(
  new ManyOf<int>(1,2,3,4)
);
ToLookup Not available yet
Union
var enu = new Distinct<int>(
  new Joined<int>(
    new ManyOf<int>(1,2,3,4),
    new ManyOf<int>(3,4,5,6)
  ).Value()
).Value(); //enu ={1,2,3,4,5,6}
Where
var newFiltered = new Filtered<string>(
  new List<string>() { "A", "B", "C" },
  (input) => input != "B"
); //newFiltered contains A & C
Zip Not available yet