Wednesday, 20 May 2015

Custom Collection with an Indexer

Let's say that for some reason you'd like to create your own collection by inheriting the CollectionBase Class. This will give you a starting point, and leverage existing collection functionality in a consistent way.

We are also going create an indexer for our custom collection 

In C#, indexers are referred to as smart arrays. A C# indexer is a class property that allows you to access a member variable of that class or struct similarly to what you would do with an index of an array. In C#, indexers are created using this keyword. Indexers in can be used in both classes and structs.

in our case we'll be able to access our custom collection members using an index.

        //access our data using an indexer
        Console.WriteLine(personCollection[1]);

As always, let's start by create our console application with a main function.

dotnet new console -n pav.customCollection --use-program-main

next let's open our newly created application in ms code

code ./pav.customCollection

your terminal could look something like this


We're going to reuse our person class, in which we create people using their name and birthdate

Then we are going to create our custom collection and we are just going to wrap our basic functions, but for example if you wanted to you could modify the inputs, or do whatever else you'd like
We'll also create an indexer for our custom collection, this will let us use integers to access our list items, but this could have been implemented using keys.


using System.Collections;

namespace pav.customCollection;

class Person : IComparable<Person>
{
    public string Name { get; set; }
    public DateTime BirthDate { get; set; }
    public Person(string Name, DateTime BirthDate)
    {
        this.Name = Name;
        this.BirthDate = BirthDate;
    }
    public int GetAge()
    {
        DateTime today = DateTime.Today;
        int age = today.Year - BirthDate.Year;
        return BirthDate > today.AddYears(-age) ? --age : age;
    }
    public override string ToString() { return $"{Name} is {GetAge()}"; }
    public int CompareTo(Person? other)
    {
        if (other != null)
            return GetAge() - other.GetAge();
        return 0;
    }
}

class PersonCollection : CollectionBase
{
    public void Add(Person person)
    {
        base.List.Add(person);
    }
    public void Insert(int index, Person person)
    {
        base.List.Insert(index, person);
    }
    public void Remove(Person person)
    {
        base.List.Remove(person);
    }

    //our indexer
    public Person this[int index]
    {
        get {
            var p = base.List[index] as Person;

            if(p != null)
                return p;
            throw new NullReferenceException($"no member found at index: {index}");
            }
        set { base.List[index] = value; }
    }
}
class Program
{

    static void Main(string[] args)
    {
        var personCollection = new PersonCollection();

        personCollection.Add(new Person("Pavel", new DateTime(1984, 01, 31)));
        personCollection.Add(new Person("Tomek", new DateTime(1988, 08, 28)));
        personCollection.Add(new Person("Magda", new DateTime(1984, 06, 28)));

        foreach (Person p in personCollection)
            Console.WriteLine(p.ToString());

        Console.WriteLine();
       
        //access our data using an indexer
        Console.WriteLine(personCollection[1]);
        personCollection[1] = new Person("marin", new DateTime(1983, 11,2));
        Console.WriteLine(personCollection[1]);
    }
}


This base class gives us the basics of what's needed to implement our own collection and we added the indexer for good measure.


Notice that our indexer let's us read and write to our list items using and index.

       
//access our data using an indexer
        Console.WriteLine(personCollection[1]);
        personCollection[1] = new Person("marin", new DateTime(1983, 11,2));
        Console.WriteLine(personCollection[1]);

Friday, 15 May 2015

List

A list<T> is very much like an arrayList, but instead of storing objects, it stores a defined type. Obviously polymorphisim applies, meaning that if your list can handle the type specified and any subtypes that derive from that type. 

to get started let's create a simple console application with an old-school main function.

dotnet new console -n pav.listExample --use-program-main

once that is done, let's open it up in code

code ./pavlistExample 

if you are using your terminal inside ms code, you can optionally add the -r tag to open your newly created console app in the current running instance of code.

code ./pav.listExample -r

your terminal would look something like this


When your application opens, you should be prompted to add some Build and Debug assets, make sure to click yes.



With that all out of the way, let's create our example:
  • create a base Person class that implements IComparable on the Person's age
  • create an employee class that inherits form our person class
in our main
  • create a list of people and fill it with both instances of Person and Employee
  • sort the list, the list will use the Person's implementation of IComparable

namespace pav.ListExample
{
    class Person : IComparable<Person>
    {
        public string Name { get; set; }
        public DateTime BirthDate { get; set; }
        public Person(string Name, DateTime BirthDate) {
            this.Name = Name;
            this.BirthDate = BirthDate;
        }
        public int GetAge() {
            DateTime today = DateTime.Today;
            int age = today.Year - BirthDate.Year;
            return BirthDate > today.AddYears(-age) ? --age : age;
        }
        public virtual string Display() { return $"{Name} is {GetAge()}"; }
        public int CompareTo(Person? other)
        {
            if(other != null)
                return GetAge() - other.GetAge();
            return 0;
        }
    }

    class Employee : Person
    {
        static int _runningID;
        public int Id { get; private set; } = _runningID++;
        public Employee(string Name, DateTime BirthDate) : base(Name, BirthDate) {}

        public override string Display() { return $"{base.Display()} with id:{Id}"; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            List<Person> l = new List<Person>();

            var p1 = new Person("Pavel", new DateTime(1984, 01, 31));
            var p2 = new Person("Tomek", new DateTime(1988, 08, 28));
            var p3 = new Person("Magda", new DateTime(1984, 06, 28));
            var e1 = new Employee("Marin", new DateTime(1983, 12, 04));
            var e2 = new Employee("Trish", new DateTime(1987, 05, 12));

            l.Add(p1);
            l.Add(p2);
            l.Add(p3);
            l.Add(e1);
            l.Add(e2);

            l.Sort();

            foreach (Person p in l)
                Console.WriteLine(p.Display());
        }
    }
}


Since our employee class derives from person our list can store both types, and since we marked our Display function as virtual when we override it in our Employee superclass when we call it on the list items we actually get the one defined in Employee instead of Person.


The above code implements IComparable so it'll sort the list of Persons and Employees youngest to oldest. if it didn't implement the interface it would just throw an exception

Sunday, 10 May 2015

Dictionary

Dictionary<TKey, TValue> is a key value pair collection, where you specify the type for both the key and the value.

to get started, let's create an oldschool console application with a main

dotnet new console -n pav.dictionaryExample --use-program-main

with our console application created open it in ms code

code ./pav.dictionaryExample 

and that should open your newly created dotnetcore project.

in our example let's create a person class,  we are going 


namespace DictionaryExample
{
    class Person
    {
        public string Name { get; set; }
        public DateTime BirthDate { get; set; }

        public Person(string Name, DateTime BirthDate) {
            this.Name = Name;
            this.BirthDate = BirthDate;
        }

        public int GetAge() {
            DateTime today = DateTime.Today;
            int age = today.Year - BirthDate.Year;
            return BirthDate > today.AddYears(-age) ? --age : age;
        }

        public override string ToString() { return $"{Name} is {GetAge()}"; }
    }

    class Program {
        static void Main(string[] args) {
            var d = new Dictionary<string, Person>();

            var p1 = new Person("Pavel", new DateTime(1984, 01, 31));
            var p2 = new Person("Tomek", new DateTime(1988, 08, 28));
            var p3 = new Person("Magda", new DateTime(1984, 06, 28));

            d.Add(p1.Name, p1);
            d.Add(p2.Name, p2);
            d.Add(p3.Name, p3);

            foreach (KeyValuePair<string, Person> de in d)
                Console.WriteLine("{0}: {1}", de.Key, de.Value.ToString());


            //print out a direct member of the dictionary by itS defined key.
            Console.WriteLine (d["Tomek"]);
        }
    }
}



if you run the above code you get the following:



if you want to remove and element by it's key you can simply call


d.Remove("Pavel");


if your collection doesn't contain this key nothing happens which is nice, if you want to retrieve an item using the key, it's just as simple.


            Console.WriteLine("get an object by a non-existent key");
            Console.WriteLine (d["marin"]);


Except for the exception that gets thrown if you query for a key that doesn't exist.



which is why it is always a good practice to first check is the key is contained in the collection


            Console.WriteLine("get an object by a non-existent key");
            if(d.ContainsKey("marin"))
                Console.WriteLine (d["marin"]);
             

The dictionary class also contains a containsValue method which works the same way, but obviously would have a performance impact. They key property is optimized for retrieving data.


            var p4 = new Person("Marin", new DateTime(1983, 11, 02));
            if (!d.ContainsValue(p4))
                d.Add(p4.Name, p4);


one caveat is that searching by value is not exactly efficient, search by key when you can.

Thursday, 7 May 2015

SortedList

A SortedList is a list that has both a key and an index, it also maintains order by the key as things are added, assuming that the key implements IComparable.

to get started in your command line create a Console application with the old school main method

dotnet new console -n pav.sortedListExample  --use-program-main

once you've created your console app, navigate into it with 

cd pav.sortedListExample

after that open it in your ms code with 

code .

Now Let's code up an example


using System.Collections;

namespace pc.SortedListExample
{
    class Program
    {
        static void Main(string[] args)
        {
            SortedList sl = new SortedList();

            sl.Add(3, "Three");
            sl.Add(7, "Seven");
            sl.Add(2, "Two");
            sl.Add(6, 6.0);
            sl.Add(4, 4);
            sl.Add(5, "Five");
            sl.Add(1, "One");

            foreach (DictionaryEntry i in sl)
                Console.WriteLine("{0}: {1}", i.Key, i.Value);

            for (int i = 0; i < sl.Count; i++)
                Console.WriteLine(sl[i]);
        }
    }

}


If you run the above code, you'll notice that the collection is sorted, on the key, this is only possible because int implements the IComparable interface. 

If it didn't then you would get an "Unhandled exception. System.InvalidOperationException: Failed to compare two elements in the array."

In this iteration, rather than using an int, I simply wrap an integer and do not implement the IComparable interface, resulting in an error.


using System.Collections;

namespace pc.SortedListExample
{
    public class MyId //: IComparable
    {
        private static int runningId = 0;
        public int Value { get; }
        public MyId(){ Value = runningId++; }
        public MyId(int value){ Value = value; }

        // public int CompareTo(object? obj)
        // {
 
        //      if(obj as MyId != null)
        //         return this.Value.CompareTo(((MyId)obj).Value);
        //     return 0;
        // }
    }
   
    class Program
    {
        static void Main(string[] args)
        {
            SortedList sl = new SortedList();
           
            sl.Add(new MyId(), "Three");
            sl.Add(new MyId(), "Seven");
            sl.Add(new MyId(), "Two");
            sl.Add(new MyId(), 6.0);
            sl.Add(new MyId(), 4);
            sl.Add(new MyId(), "Five");
            sl.Add(new MyId(), "One");

            foreach (DictionaryEntry de in sl)
                Console.WriteLine("{0}: {1}", ((MyId)de.Key).Value, de.Value);

            for (int i = 0; i < sl.Count; i++)
                Console.WriteLine(sl[new MyId(i)]);
        }
    }

}




To fix this all I simply have to do is implement the IComparable interface on myId class and everything will work again.


using System.Collections;

namespace pc.SortedListExample
{
    public class MyId : IComparable
    {
        private static int runningId = 0;
        public int Value { get; }
        public MyId(){ Value = runningId++; }
        public MyId(int value){ Value = value; }

        public int CompareTo(object? obj)
        {
             if(obj as MyId != null)
                return this.Value.CompareTo(((MyId)obj).Value);
            return 0;
        }
    }
   
    class Program
    {
        static void Main(string[] args)
        {
            SortedList sl = new SortedList();
           
            sl.Add(new MyId(), "Three");
            sl.Add(new MyId(), "Seven");
            sl.Add(new MyId(), "Two");
            sl.Add(new MyId(), 6.0);
            sl.Add(new MyId(), 4);
            sl.Add(new MyId(), "Five");
            sl.Add(new MyId(), "One");

            foreach (DictionaryEntry de in sl)
                Console.WriteLine("{0}: {1}", ((MyId)de.Key).Value, de.Value);

            for (int i = 0; i < sl.Count; i++)
                Console.WriteLine(sl[new MyId(i)]);
        }
    }

}


Notice that we can either iterate using a foreach or display things using the index. Sorted lists work best with one type for your key, if your keys are made of different types then using a hashtable is more effective.

Tuesday, 5 May 2015

Stack

The stack is similar to a queue but the opposite, a stack is a First-in-Last-out or FiLo data structure. Think of a stack of cookies, you stack one on top of the other and when you go to eat your first cookie, it's the last one you put down. 



though a stack has several functions, the three main ones are:
  • Push: adds an item to the top of the stack
  • Pop: takes out the newest item from the top of stack
  • Peek: looks at the newest item on the top of the stack.

Now let's look at a demo


using System.Collections;

namespace pav.stackExample;
class Program
{
    static void Main(string[] args)
    {
        Stack s = new Stack();

        s.Push(1);
        s.Push("two");
        s.Push("Three");
        s.Push(4);
        s.Push(5.0);

        while (s.Count > 0)
            Console.WriteLine(s.Pop());
    }

}




one thing to keep in mind is that peek an empty stack will through an exception.

Monday, 4 May 2015

Queue

The queue is a First in First out data structure or fifo, it's the way your printer "Queue" works. The c# implementation of the queue has various additional functions, you can read all about it on the MSDN docs but at its core the queue uses the following:
  • Enqueue: adds an item to the queue
  • Dequeue: returns & removes the oldest item in the queue
  • Peek: looks at the oldest item in the queue 
You can think of it as a line-up of people to get ice cream, whoever get's in line first, gets served first 


That is pretty much if for the most basic of queues, notice that in our implementation of our queue below, we mix and match our types, the basic implementation of the queue is indifferent to what type of object is stored inside it, it's not used for sorting, it is a sequential data-structure used to maintain order


namespace pav.queue;

using System;
using System.Collections;

public class Person{
public string Name { get; set; }
public Person(string name) => this.Name = name;
public override string ToString() => this.Name;
}

class Program
{
static void Main(string[] args)
{
var q = new Queue();

q.Enqueue("one");
q.Enqueue("Two");
q.Enqueue(3);
q.Enqueue("Four");
q.Enqueue("Five");
q.Enqueue(new Person("pawel"));
q.Enqueue(6.0);
q.Enqueue("Seven");

Console.WriteLine("Count after enqueue {0}\n", q.Count);

while (q.Count > 0)
Console.WriteLine(q.Dequeue());

Console.WriteLine("\nCount after Dequeue {0}", q.Count);
}
}


The queue is exactly what it sounds like, it's a line of things in which the thing at the front of the line is next and anything added after goes to the back.

One thing to make not of is that there is a more modern implementation of the Queue using generics, this let's you restrict your queue's data type, you can read about it again on the MSDN docs

A Quick implementation would simply be to restrict our queue above to only accept strings.


namespace pav.queue;

using System;
using System.Collections;

public class Person{
public string Name { get; set; }
public Person(string name) => this.Name = name;
public override string ToString() => this.Name;
}

class Program
{
static void Main(string[] args)
{
var q = new Queue<string>();

q.Enqueue("one");
q.Enqueue("Two");
//q.Enqueue(3);
q.Enqueue("Four");
q.Enqueue("Five");
//q.Enqueue(new Person("pawel"));
//q.Enqueue(6.0);
q.Enqueue("Seven");

Console.WriteLine("Count after enqueue {0}\n", q.Count);

while (q.Count > 0)
Console.WriteLine(q.Dequeue());

Console.WriteLine("\nCount after Dequeue {0}", q.Count);
}
}


Notice that when we declare our Queue, we specify the type which the queue will contain.

Hashtable

A hashtable is a data structure that maps key value pairs. A hashtable leverages a hash function to compute an index into an array of slots, from which the value can be found.

You can think of it as an array that uses a function to get it's index from a key. here's a more in depth explanation. now for large data sets especially ones that have a unique key this data structure is perfect.

using System;
using System.Collections;

namespace pc.hashtableexample
{
class Program
{
public class Person
{
public string Name { get; set; }
public DateTime BirthDate { get; set; }
public Person(string Name, DateTime BirthDate)
{
this.Name = Name;
this.BirthDate = BirthDate;
}
public int GetAge()
{
DateTime today = DateTime.Today;
int age = today.Year - BirthDate.Year;
return BirthDate > today.AddYears(-age) ? --age : age;
}

public override string ToString()
{
return Name + " is " + GetAge();
}
}

static void Main(string[] args)
{
var ht = new Hashtable();

//add key and item
ht.Add(2, new Person("Pawel", new DateTime(1984, 01, 31)));
ht.Add("bla", new Person("Tom", new DateTime(1988, 08, 28)));
ht.Add(3.0, new Person("Magda", new DateTime(1984, 06, 28)));

Console.WriteLine(ht["bla"]);
Console.WriteLine(ht[2]);
Console.WriteLine(ht[3]);
Console.WriteLine(ht[3.0]);
}
}
}


Above you can see how simple it is to use the hash table data-structure, just keep in mind the following: 
  •  calling fr a key that doesn't exist return null, 
  •  anything that derives from object can be a key 
  • one last caveat is that the key must be unique

Sunday, 3 May 2015

ArrayList

An ArrayList is an interesting data structure that lets us create a list of different types of objects, that is we can cast classes as objects and box types.

Lets start by first create a simple person class.

   
    public class Person
    {
        public string Name { get; set; }
        public int BirthYear { get; set; }
        public Person(string Name, int BirthYear)
        {
            this.Name = Name;
            this.BirthYear = BirthYear;
        }

        public override string ToString()
        {
            return $"{this.Name} born {this.BirthYear}";
        }
    }


Just as before a very simple class which allows us to instantiate an object with two properties constructing a representation of a person.

Now in our main let's create an arraylist of various types


    using System.Collections;

    var arrayList = new ArrayList() { 1, "two", 3.4, 'a', new char[] { 'a', 'b', 'c' }, new Person("Pawel", 84) };

    var i = 0;
    foreach (var o in arrayList){
        var t = o.ToString().Length < 7 ? "\t\t" : "";
        Console.WriteLine($"{i++}) {o}\t{t} is of type {o.GetType()}");
    }

    //Gets or sets the number of elements in the ArrayList
    Console.WriteLine($"\nCapacity of an arraylist included sub-array counts {arrayList.Capacity}");

    //Gets the number of actual elements in the ArrayList
    Console.WriteLine($"Count of an array list only returns the number of elements in the array list {arrayList.Count}\n");



this lets us hold a cornucopia of types in one collection. In the above we have, Integers, characters, character arrays and even our own Person type.

One caveat to be aware of is that if we try to use the 'sort' method on our array list, each element within the collection must implement the IComparable interface and each element must be comparable with all other in the arraylist which could prove to be challenging. In instances of hierarchy, it is pretty straight forward, just ensure that your base class implements the IComparable interface, and you should be ok, for example if you have an 'Employee' class which inherits from a 'Person' class, then you can add both to an array list and implement the 'sort' method, just like you would in a regular array of Persons.

    
    public class Person : IComparable {
        public DateOnly Birthdate { get; set; }
        public string Name { get; set; }

        public Person(string name, DateOnly birthdate)
        {
                this.Name = name;
                this.Birthdate = birthdate;
        }
        public virtual int Age {
            get{
                // Save today's date.
                var today = DateTime.Today;

                // Calculate the age.
                var age = today.Year - Birthdate.Year;

                // Go back to the year in which the person was born in case of a leap year
                if (Birthdate.Year > today.AddYears(-age).Year) age--;
                    return age;
            }
        }

        public override string ToString()
        {
            return $" {this.Name} is a {this.GetType().Name} and is {this.Age} years old";
        }

        public int CompareTo(object? obj)
        {
            var other = (Person)obj;
            if(other != null)
                if(this.Age - other.Age < 0)
                    return   1;
                else if(this.Age - other.Age > 0)
                    return -1;
            return 0;
        }
    }

    public class Employee : Person
    {
        private static int runningId = 0;
        public int idNumber { get; set; }
   
        public Employee(string name, DateOnly birthdate) : base(name, birthdate)
        {
            this.idNumber = ++runningId;
        }

        public override string ToString()
        {
            return $"{base.ToString()} with id Number: {idNumber}";
        }
    }


Since we implemented the IComparable interface on our base Person class we can easily use the Array 'sort' method.


    using System.Collections;
    internal class Program
    {
        private static void Main(string[] args)
        {
            Console.WriteLine("\nArray list:");

            var arrayList = new ArrayList() {
                new Person("Pawel", new DateOnly(1984, 01, 31)),
                new Employee("John", new DateOnly(1988, 2, 12))
            };

            arrayList.Sort();

            foreach (var person in arrayList)
                Console.WriteLine(person);

            Console.WriteLine("\nNormal array:");
            var array = new Person[] {
                new Person("Pawel", new DateOnly(1984, 01, 31)),
                new Employee("John", new DateOnly(1988, 2, 12))
            };
    
            Array.Sort(array);
            foreach (var person in array)
                Console.WriteLine(person);
        }
    }


As you can see since employee inherits from person it leverages persons implementation of the IComparable interface letting us sort our array list, had we added an object that didn't inherit from person and couldn't be compared to a person then we'd get an exception.

However, if as below we really stretched it and implemented the IComparable interface in such a fashion that we could compare different objects to each other that would still be valid as shown below in our redefined Person class followed by our new Dog class.


    public class Person : IComparable {
        public DateOnly Birthdate { get; set; }
        public string Name { get; set; }

        public Person(string name, DateOnly birthdate)
        {
                this.Name = name;
                this.Birthdate = birthdate;
        }
        public virtual int Age {
            get{
                // Save today's date.
                var today = DateTime.Today;

                // Calculate the age.
                var age = today.Year - Birthdate.Year;

                // Go back to the year in which the person was born in case of a leap year
                if (Birthdate.Year > today.AddYears(-age).Year) age--;
                    return age;
            }
        }

        public override string ToString()
        {
            return $" {this.Name} is a {this.GetType().Name} and is {this.Age} years old";
        }

        public int CompareTo(object? obj)
        {
            dynamic otherobj as Person != null ? (Person)obj : obj as Dog != null ? (Dog)obj
                : throw new NullReferenceException();  

            if(other != null)
                if(this.Age - other.Age < 0)
                    return   1;
                else if(this.Age - other.Age > 0)
                    return -1;
            return 0;
        }
    }

    public class Dog : IComparable
    {
        public DateOnly Birthdate { get; set; }
        public string Name { get; set; }
       
        public Dog(string name, DateOnly birthdate)
        {
            this.Name = name;
            this.Birthdate = birthdate;
        }

        public int Age {
            get{
                // Save today's date.
                var today = DateTime.Today;

                // Calculate the age.
                var age = today.Year - Birthdate.Year;

                // Go back to the year in which the person was born in case of a leap year
                if (Birthdate.Year > today.AddYears(-age).Year) age--;
                return age * 7;
            }
        }

        public override string ToString()
        {
            return $" {this.Name} is a {this.GetType().Name} and is {this.Age} years old";
        }

        public int CompareTo(object? obj)
        {
            dynamic other = obj as Person != null ? (Person)obj : obj as Dog != null ? (Dog)obj
                :  throw new NullReferenceException();

            if(other != null)
                if(this.Age - other.Age < 0)
                    return   1;
                else if(this.Age - other.Age > 0)
                    return -1;
            return 0;
        }
    }


Notice that in the above we introduced the dynmaic keyword for our object type, that just means that our other type will be defined at runtime and not compile time. Now if we run our program,


    using System.Collections;

    internal class Program
    {
        private static void Main(string[] args)
        {
            Console.WriteLine("\nArray list:");

            var arrayList = new ArrayList() {
                new Person("Pawel", new DateOnly(1984, 01, 31)),
                new Employee("John", new DateOnly(1988, 2, 12)),
                new Dog("Spot", new DateOnly(2011,2,15)),
                new Dog("Kitcha", new DateOnly(2020,2,15))
            };
   
            arrayList.Sort();

            foreach (var person in arrayList)
                Console.WriteLine(person);

            Console.WriteLine("\nNormal array:");
            var array = new Person[] {
                new Person("Pawel", new DateOnly(1984, 01, 31)),
                new Employee("John", new DateOnly(1988, 2, 12))
            };

            Array.Sort(array);
            foreach (var person in array)
                Console.WriteLine(person);
        }
    }


we see that in our array list, we have two distinct types 


now just because we can, doesn't mean we should, however if you must, rather than implementing the same code in the Dog and person class, we can extract it and create a PersonToDogComparrer, as is shown in the next example.


    using System.Collections;

    public class Person
    {
        public DateOnly Birthdate { get; set; }
        public string Name { get; set; }

        public Person(string name, DateOnly birthdate)
        {
            this.Name = name;
            this.Birthdate = birthdate;
        }
        public virtual int Age
        {
            get
            {
                var today = DateTime.Today;
                var age = today.Year - Birthdate.Year;

                if (Birthdate.Year > today.AddYears(-age).Year) age--;
                    return age;
            }
        }

        public override string ToString()
        {
            return $" {this.Name} is a {this.GetType().Name} and is {this.Age} years old";
        }
    }

    public class Dog
    {
        public DateOnly Birthdate { get; set; }
        public string Name { get; set; }

        public Dog(string name, DateOnly birthdate)
        {
            this.Name = name;
            this.Birthdate = birthdate;
        }

        public int Age
        {
            get
            {
                var today = DateTime.Today;
                var age = today.Year - Birthdate.Year;

                if (Birthdate.Year > today.AddYears(-age).Year) age--;
                return age * 7;
            }
        }

        public override string ToString()
        {
            return $" {this.Name} is a {this.GetType().Name} and is {this.Age} years old";
        }
    }

    public class Employee : Person
    {
        private static int runningId = 0;
        public int idNumber { get; set; }
        public Employee(string name, DateOnly birthdate) : base(name, birthdate)
        {
            this.idNumber = ++runningId;
        }

        public override string ToString()
        {
            return $"{base.ToString()} with id Number: {idNumber}";
        }
    }

    class PersonToDogComparer : IComparer
    {
        public int Compare(object? objX, object? objY)
        {
            dynamic This = objX as Person != null ? (Person)objX : objX as Dog != null ? (Dog)objX :
                throw new NullReferenceException();
            dynamic That = objY as Person != null ? (Person)objY : objY as Dog != null ? (Dog)objY :
                throw new NullReferenceException();

            if (This != null)
                if (This.Age - That.Age < 0)
                    return 1;
                else if (This.Age - That.Age > 0)
                    return -1;
            return 0;
        }
    }

    internal class Program
    {
        private static void Main(string[] args)
        {
            Console.WriteLine("\nArray list:");

            var arrayList = new ArrayList() {
                new Person("Pawel", new DateOnly(1984, 01, 31)),
                new Employee("John", new DateOnly(1988, 2, 12)),
                new Dog("Spot", new DateOnly(2011,2,15)),
                new Dog("Kitcha", new DateOnly(2020,2,15))
            };


            arrayList.Sort(new PersonToDogComparer());

            foreach (var person in arrayList)
                Console.WriteLine(person);
        }
    }


In the code above we extracted our compare logic into its own class which implements the 'IComparer' interface, then we pass an instance of that class to our sort method.