//Copyright (C) Troy Magennis

using System;
using System.Collections;   
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using System.Xml.Linq;
using SampleSupport;
using QuerySamples;
using System.Xml;
using System.Data.SqlClient;
using System.Text;
using System.Drawing;


namespace SampleQueries
{
    [Title("Rozdział 4 - Grupowanie i łączenie danych")]
    [Prefix("Listing_4_")]
    public class Chapter04Samples : SampleHarness
    {
        #region Sample Data

        public class Contact
        {
            public string FirstName { get; set; }
            public string LastName { get; set; }
            public string Email { get; set; }
            public string Phone { get; set; }
            public string Extension { get; set; }
            public DateTime DateOfBirth { get; set; }
            public string State { get; set; }

            public static List<Contact> SampleData()
            {
                return new List<Contact> {
                    new Contact {FirstName = "Bartłomiej", LastName = "Gajewski",      DateOfBirth = new DateTime(1945,10,19), Phone = "885 983 885", Email = "gajewski@aspiring-technology.com", State = "MA" },
                    //new Contact {FirstName = "Mateusz",    LastName = "Gajewski",      State = "MA" },
                    //new Contact {FirstName = "Sławomir",   LastName = "Gajewski",      State = "WM" },
                    new Contact {FirstName = "Alfred",     LastName = "Wieczorek",     DateOfBirth = new DateTime(1973,12,09), Phone = "848 553 848", Email = "al1@aspiring-technology.com", State = "WM" },
                    new Contact {FirstName = "Adam",       LastName = "Gadomski",      DateOfBirth = new DateTime(1959,10,03), Phone = "115 999 115", Email = "adamg@aspiring-technology.com", State = "OP" },  
                    //new Contact {FirstName = "Adam",       LastName = "Gadomski",      State = "OP" },  
                    //new Contact {FirstName = "Adam",       LastName = "Gadomski",      State = "MA" },  
                    new Contact {FirstName = "Jan",        LastName = "Detka",         DateOfBirth = new DateTime(1950,12,16), Phone = "677 602 677", Email = "jan.detka@aspiring-technology.com", State = "MA" },
                    new Contact {FirstName = "Cezary",     LastName = "Zbytek",        DateOfBirth = new DateTime(1935,02,10), Phone = "603 303 603", Email = "czbytek@aspiring-technology.com", State = "LU" },
                    new Contact {FirstName = "Stanisław",  LastName = "Kowal",         DateOfBirth = new DateTime(1950,02,20), Phone = "546 607 546", Email = "kowals@aspiring-technology.com", State = "WM" },
                    new Contact {FirstName = "Cyryl",      LastName = "Latos",         DateOfBirth = new DateTime(1951,10,21), Phone = "278 918 278", Email = "latos@aspiring-technology.com", State = "WM" },
                    new Contact {FirstName = "Bernard",    LastName = "Radliński",     DateOfBirth = new DateTime(1946,05,18), Phone = "715 920 715", Email = "bernard@aspiring-technology.com", State = "WP" },
                    new Contact {FirstName = "Maciej",     LastName = "Karaś",         DateOfBirth = new DateTime(1977,09,17), Phone = "364 202 364", Email = "mac.karas@aspiring-technology.com", State = "WP" },
                    new Contact {FirstName = "Adrian",     LastName = "Hawrat",        DateOfBirth = new DateTime(1922,05,23), Phone = "165 737 165", Email = "adrianh@aspiring-technology.com", State = "SW" }
                };

            }
        }

        public class CallLog
        {
            public string Number { get; set; }
            public int Duration { get; set; }
            public bool Incoming { get; set; }
            public DateTime When { get; set; }
            public string Extension { get; set; }

            public static List<CallLog> SampleData()
            {
                return new List<CallLog> {
                    new CallLog { Number = "885 983 885", Extension = "", Duration = 2,  Incoming = true,  When = new DateTime(2006,	8,	7,	8,	12,	0)},
                    new CallLog { Number = "165 737 165", Extension = "", Duration = 15, Incoming = true,  When = new DateTime(2006,	8,	7,	9,	23,	0) },
                    new CallLog { Number = "364 202 364", Extension = "", Duration = 1,  Incoming = false, When = new DateTime(2006,	8,	7,	10,	5,	0) },
                    new CallLog { Number = "603 303 603", Extension = "", Duration = 2,  Incoming = false, When = new DateTime(2006,	8,	7,	10,	35,	0) },
                    new CallLog { Number = "546 607 546", Extension = "", Duration = 4,  Incoming = true,  When = new DateTime(2006,	8,	7,	11,	15,	0) },
                    new CallLog { Number = "885 983 885", Extension = "", Duration = 15, Incoming = false, When = new DateTime(2006,	8,	7,	13,	12,	0) },
                    new CallLog { Number = "885 983 885", Extension = "", Duration = 3,  Incoming = true,  When = new DateTime(2006,	8,	7,	13,	47,	0) },
                    new CallLog { Number = "546 607 546", Extension = "", Duration = 1,  Incoming = false, When = new DateTime(2006,	8,	7,	20,	34,	0) },
                    new CallLog { Number = "546 607 546", Extension = "", Duration = 3,  Incoming = false, When = new DateTime(2006,	8,	8,	10,	10,	0) },
                    new CallLog { Number = "603 303 603", Extension = "", Duration = 23, Incoming = false, When = new DateTime(2006,	8,	8,	10,	40,	0) },
                    new CallLog { Number = "848 553 848", Extension = "", Duration = 3,  Incoming = false, When = new DateTime(2006,	8,	8,	14,	0,	0) },
                    new CallLog { Number = "848 553 848", Extension = "", Duration = 7,  Incoming = true,  When = new DateTime(2006,	8,	8,	14,	37,	0) },
                    new CallLog { Number = "278 918 278", Extension = "", Duration = 6,  Incoming = true,  When = new DateTime(2006,	8,	8,	15,	23,	0) },
                    new CallLog { Number = "364 202 364", Extension = "", Duration = 20, Incoming = true,  When = new DateTime(2006,	8,	8,	17,	12,	0) },
                    new CallLog { Number = "885 983 885", Extension = "", Duration = 5,  Incoming = true,  When = new DateTime(2006,	7,	12,	8,	12,	0)},
                    new CallLog { Number = "165 737 165", Extension = "", Duration = 12, Incoming = true,  When = new DateTime(2006,	6,	14,	9,	23,	0) },
                    new CallLog { Number = "364 202 364", Extension = "", Duration = 10,  Incoming = false, When = new DateTime(2006,	7,	9,	10,	5,	0) },
                    new CallLog { Number = "603 303 603", Extension = "", Duration = 22,  Incoming = false, When = new DateTime(2006,	7,	5,	10,	35,	0) },
                    new CallLog { Number = "546 607 546", Extension = "", Duration = 9,  Incoming = true,  When = new DateTime(2006,	6,	7,	11,	15,	0) },
                    new CallLog { Number = "885 983 885", Extension = "", Duration = 10, Incoming = false, When = new DateTime(2006,	6,	7,	13,	12,	0) },
                    new CallLog { Number = "885 983 885", Extension = "", Duration = 21,  Incoming = true,  When = new DateTime(2006,	7,	7,	13,	47,	0) },
                    new CallLog { Number = "546 607 546", Extension = "",Duration = 7,  Incoming = false, When = new DateTime(2006,	7,	7,	20,	34,	0) },
                    new CallLog { Number = "546 607 546", Extension = "", Duration = 2,  Incoming = false, When = new DateTime(2006,	6,	8,	10,	10,	0) },
                    new CallLog { Number = "603 303 603", Extension = "", Duration = 3, Incoming = false, When = new DateTime(2006,	6,	8,	10,	40,	0) },
                    new CallLog { Number = "848 553 848", Extension = "", Duration = 32,  Incoming = false, When = new DateTime(2006,	7,	8,	14,	0,	0) },
                    new CallLog { Number = "848 553 848", Extension = "", Duration = 13,  Incoming = true,  When = new DateTime(2006,	7,	8,	14,	37,	0) },
                    new CallLog { Number = "278 918 278", Extension = "", Duration = 16,  Incoming = true,  When = new DateTime(2006,	5,	8,	15,	23,	0) },
                    new CallLog { Number = "364 202 364", Extension = "", Duration = 24, Incoming = true,  When = new DateTime(2006,	6,	8,	17,	12,	0) }
                };
            }
        }

        #endregion

        private void smallSnippets()
        {
            var contacts = Contact.SampleData();

            var q = from c in contacts
                    group c by c.State;

            var q1 = contacts.GroupBy(c => c.State);

            // w celu uniknięcia ostrzeżeń
            if (q.SequenceEqual(q1)) { };
        }


        // sygantura GroupBy
        /*
        public static IEnumerable<TResult>
            GroupBy<TSource, TKey, TElement, TResult>(
        this IEnumerable<TSource> source,
        Func<TSource, TKey> keySelector,
        Func<TSource, TElement> elementSelector, // opcjonalne
        Func<TKey, IEnumerable<TElement>, TResult>
            resultSelector, // oopcjonalne
        IEqualityComparer<TKey> comparer) // opcjonalne
        {
            return null;
        }
        */

        [Category("Grupowanie")]
        [Title("Listing 4-1 : Proste grupowanie")]
        [Description("Przykład demonstruje grupowanie oraz pracę z każdą grupą.")]
        public void Listing_4_1_Simple_Grouping()
        {
            string[] partNumbers = new string[] { "SCW10", "SCW1", 
                "SCW2", "SCW11", "NUT10", "NUT1", "NUT2", "NUT11" };
            
            var q = from pn in partNumbers
                    group pn by pn.Substring(0, 3);

            foreach (var group in q)
            {
                Console.WriteLine("Klucz grupy: {0}", group.Key);
                foreach (var part in group)
                    Console.WriteLine(" - {0}", part);
            }
        }

        [Category("Grupowanie")]
        [Title("Listing 4-2 : Użycie operatorów null-coalescing i trójskładnikowego do ochrony wyrażeń keySelector przed wartościami null")]
        [Description("Przykład demonstruje, jak obsługiwać wartości null w wyrażeniu selektora przy grupowaniu.")]
        public void Listing_4_2_Handling_Nulls_WithNCOAndTernary()
        {
            // Ochrona przeciwko wartościom null za pomocą 
            // operatora trójskładnikowego (?)
            var q1 = from c in Contact.SampleData()
                     group c by
                         c.State == null ? "(null)" : c.State;

            // Ochrona przeciwko wartościom null za pomocą 
            // operatora null-coalescing (??).
            var q2 = from c in Contact.SampleData()
                     group c by
                         c.State ?? "(null)";
        }
        
        [Category("Grupowanie")]
        [Title("Listing 4 : Obsługa wartości null")]
        [Description("Przykład demonstruje, jak obsługiwać wartości null w wyrażeniu selektora przy grupowaniu.")]
        public void Listing_4_Handling_Nulls()
        {
            string[] partNumbers = new string[] { "SCW10", null, 
                "SCW2", "SCW11", null, "NUT1", "NUT2", null };

            // Rada - zapobiegaj wartościom null za pomocą 
            // operatora trójskładnikowego (?) lub null-coalescing (??).
            var q1 = from pn in partNumbers
                     group pn by 
                         pn == null ? "(null)" : pn.Substring(0,3);

            Console.WriteLine("q1 result - ");
            Console.WriteLine();
            foreach (var group in q1)
            {
                // działa tak, jak się tego spodziewamy. Wszystkie wartości null są grupowane w "(null)"
                Console.WriteLine("Group key: {0}", group.Key);
                foreach (var part in group)
                    Console.WriteLine(" - {0}", part);
            }

            // Uwaga - wartości null w danych źródłowych mogą uszkodzić
            // wyrażenie grupowania. Poniższe grupowanie nie powiedzie się.
            var q2 = from pn in partNumbers
                     group pn by pn.Substring(0, 3);

            Console.WriteLine();
            Console.WriteLine("wynik q2 - nie obsługuje wartości null -");
            Console.WriteLine();
            foreach (var group in q2)
            {
                // nie działa i zgłasza wyjątek null!
            }

        }


        [Category("Grupowanie")]
        [Title("Listing 4-3 : Grupowanie na podstawie kilku wartości.")]
        [Description("Przykład demonstruje, w jaki sposób typy anonimowe mogą posłużyć do grupowania na podstawie więcej niż jednej wartości.")]
        [LinkedClass("Contact")]
        public void Listing_4_3_Grouping_Multiple_Values()
        {
            /* Ten przykład korzysta z tych samych danych, które widzieliśmy w tabeli 2.2
               jednak dodałem dwóch Gajewskich (jednego z tego samego województwa i 
               drugiego z innego) oraz dwóch Gadomskich
            

                    Imię         Nazwisko   Województwo
                    -----------------------------------
                    Bartłomiej   Gajewski       MA
                    Mateusz      Gajewski       MA  *dodany
                    Sławomir     Gajewski       WM  *dodany
                    Alfred       Wieczorek      WM  *dodany
                    Adam         Gadomski       OP
                    Adam         Gadomski       OP  *dodany
                    Adam         Gadomski       MA  *dodany
                    Jan          Detka          MA
                    Cezary       Zbytek         LU
                    Stanisław    Kowal          WM
                    Cyryl        Latos          WM
                    Bernard      Radliński      WP
                    Maciej       Karaś          WP
                    Adrian       Hawrat         SW
            */

            var q = from c in Contact.SampleData()
                    group c by new { c.LastName, c.State };

            foreach (var grp in q)
            {
                Console.WriteLine("Groupa - {0}, {1} - liczba = {2}", 
                    grp.Key.LastName, 
                    grp.Key.State,
                    grp.Count());
            }
        }

        [Category("Grupowanie")]
        [Title("Listing 4-4 : Grupowanie na podstawie kilku wartości")]
        [Description("Przykład demonstruje, jak użyć typu konkretnego typu do pogrupowania na podstawie kilku wartości.")]
        [LinkedClass("LastNameState")]
        [LinkedClass("Contact")]
        public void Listing_4_4_Grouping_Multiple_Values()
        {
            var q = from c in Contact.SampleData()
                    group c by new LastNameState { 
                        LastName = c.LastName, State = c.State };

            foreach (var grp in q)
            {
                Console.WriteLine("Groupa - {0}, {1} - liczba = {2}",
                    grp.Key.LastName,
                    grp.Key.State,
                    grp.Count());
            }
        }

        public class LastNameState
        {
            public string LastName { get; set; }
            public string State { get; set; }

            // zapoznaj się ze wskazówkami MSDN -
            // http://msdn.microsoft.com/en-us/library/
            //                     ms173147(VS.80).aspx
            public override bool Equals(object obj)
            {
                if (this != obj)
                {
                    LastNameState item = obj as LastNameState;
                    if (item == null) return false;
                    if (State != item.State) return false;
                    if (LastName != item.LastName) return false;
                }

                return true;
            }

            // zapoznaj się ze wskazówkami MSDN - 
            // http://msdn.microsoft.com/en-us/library/
            //           system.object.gethashcode.aspx
            public override int GetHashCode()
            {
                int result = State != null ? State.GetHashCode() : 1;
                result = result ^ 
                      (LastName != null ? LastName.GetHashCode() : 2);

                return result;
            }
        }

        [Category("Grupowanie")]
        [Title("Listing 4-5 : Własny komparator równości")]
        [Description("Przykład demonstruje, jak skonstruować własny komparator równości Soundex.")]
        [LinkedClass("SoundexEqualityComparer")]
        public void Listing_4_5_Soundex()
        {
            // przykład tylko w celu pokazania komparatora równości Soundex.
        }

        public class SoundexEqualityComparer
            : IEqualityComparer<string>
        {
            public bool Equals(string x, string y)
            {
                return GetHashCode(x) == GetHashCode(y);
            }

            public int GetHashCode(string obj)
            {
                // Np. konwertowanie kodu Soundex A123,
                // na liczbę całkowitą: 65123
                int result = 0;

                string s = soundex(obj);
                if (string.IsNullOrEmpty(s) == false)
                    result = Convert.ToInt32(s[0]) * 1000 +
                             Convert.ToInt32(s.Substring(1, 3));

                return result;
            }

            private string soundex(string s)
            {
                // Algorytm jak opisany na
                //     http://en.wikipedia.org/wiki/Soundex.
                // tworzy kody ciągów znaków w formacie: 
                //     [A-Z][0-6][0-6][0-6]
                // na podstawie fonetyki danych wejściowych.
 
                if (String.IsNullOrEmpty(s))
                    return null;
                
                StringBuilder result =
                    new StringBuilder();

                 string source = s.ToUpper().Replace(" ", "");
                // dodawanie pierwszego znaku, następnie 
                // wykonanie mapowania znaków w pętli
                result.Append(source[0]);
                char previous = '0';

                for (int i = 1; i < source.Length; i++)
                {
                    // mapowanie do wartości numerycznej Soundex
                    char mappedTo = '0';
                    char thisChar = source[i];
                    
                    if ("BFPV".Contains(thisChar))
                        mappedTo = '1';
                    else if ("CGJKQSXZ".Contains(thisChar))
                        mappedTo = '2';
                    else if ("DT".Contains(thisChar))
                        mappedTo = '3';
                    else if ('L' == thisChar)
                        mappedTo = '4';
                    else if ("MN".Contains(thisChar))
                        mappedTo = '5';
                    else if ('R' == thisChar)
                        mappedTo = '6';

                    // ignorowanie przylegających do siebie duplikatów
                    // oraz niedopasowanych znaków
                    if (mappedTo != previous && mappedTo != '0')
                    {
                        result.Append(mappedTo);
                        previous = mappedTo;
                    }
                }

                while (result.Length < 4) 
                    result.Append("0");

                return result.ToString(0, 4);
            }
        }

        [Category("Grupowanie")]
        [Title("Listing 4-6 : Własny komparator równości")]
        [Description("Przykład demonstruje, jak używać własnego komparatora równości.")]
        [LinkedClass("SoundexEqualityComparer")]
        public void Listing_4_6_Cuatom_Equality_Comparer()
        {
            string[] names = new string[] { "Janet", "Janette", "Joanne", 
                "Jo-anne", "Johanne", "Katy", "Katie", "Ralph", "Ralphe" };

            var q = names.GroupBy(s => s, 
                new SoundexEqualityComparer());

            foreach (var group in q)
            {
                Console.WriteLine(group.Key);
                foreach (var name in group)
                    Console.WriteLine(" - {0}", name);
            }

        }

        [Category("Grupowanie")]
        [Title("Listing 4-7 : Projekcja pogrupowanych elementów do nowego typu")]
        [Description("Przykład demonstruje, jak zmienić projekcję elementów do każdej grupy.")]
        public void Listing_4_7_Cuatom_Element_Projection()
        {
            IList<Contact> contacts = Contact.SampleData();

            var q = contacts.GroupBy(
                        c => c.State, 
                        c => c.FirstName + " " + c.LastName);

            foreach (var group in q)
            {
                Console.WriteLine("Województwo: {0}", group.Key);
                foreach (string name in group)
                    Console.WriteLine("  {0}", name);
            }
        }

        [Category("Grupowanie")]
        [Title("Listing 4-8 : Projekcja pogrupowanych elementów do nowego typu")]
        [Description("Przykład demonstruje, jak zmienić projekcję elementów do każdej grupy.")]
        [LinkedClass("Contact")]
        public void Listing_4_8_Custom_Element_Projection_AnonymousType()
        {
            List<Contact> contacts = Contact.SampleData();

            var q = contacts.GroupBy(
                        c => c.State ?? "województwo nieznane",
                        c => new
                        {
                            Title = c.FirstName + " " + c.LastName,
                            Email = c.Email ?? "adres email nieznany"
                        });

            foreach (var group in q)
            {
                Console.WriteLine("Województwo: {0}", group.Key);
                foreach (var element in group)
                    Console.WriteLine("  {0} ({1})", 
                        element.Title, 
                        element.Email);
            }
        }

        [Category("Grupowanie")]
        [Title("Listing 4-9 : Kontynuacja zapytań grupowania i agregacji")]
        [Description("Przykład demonstruje, jak użyć wyników zapytania w tym samym zapytaniu i pobrać zagregowane wartości.")]
        [LinkedClass("CallLog")]
        public void Listing_4_9_Grouping_Query_Continuation()
        {
            List<CallLog> calls = CallLog.SampleData();

            var q = from c in calls
                    group c by c.Number into g
                    select new
                    {
                        Number = g.Key,
                        InCalls = g.Count(c => c.Incoming),
                        OutCalls = g.Count(c => !c.Incoming),
                        TotalTime = g.Sum(c => c.Duration),
                        AvgTime = g.Average(c => c.Duration)
                    };

            foreach (var number in q)
                Console.WriteLine(
                    "{0} ({1} przych., {2} wych.) Średni czas: {3} min.",
                    number.Number,
                    number.InCalls,
                    number.OutCalls,
                    Math.Round(number.AvgTime, 2));
        }

        [Category("Grupowanie")]
        [Title("Listing 4-10 : Grupowanie danych na kilku poziomach głębokości")]
        [Description("Przykład pokazuje, jak zagnieżdżać i agregować pogrupowane dane.")]
        [LinkedClass("Contact")]
        [LinkedClass("CallLog")]
        public void Listing_4_10_Nested_Grouping_Query_Continuation()
        {
            List<Contact> contacts = Contact.SampleData();
            List<CallLog> callLog = CallLog.SampleData();

            var q = from contact in contacts
                    select new
                    {
                        Name = contact.FirstName + " " + 
                                    contact.LastName,
                        YearGroup = from call in callLog
                                    where call.Number == contact.Phone
                                    group call by call.When.Year 
                                        into groupYear
                                    select new
                                    {
                                        Year = groupYear.Key,
                                        MonthGroup = 
                                           from c in groupYear
                                           group c by c.When.ToString("MMMM") 
                                    }
                    };
            
            foreach (var con in q)
            {
                Console.WriteLine("Klient: {0}", con.Name);
                foreach (var year in con.YearGroup)
                {
                    Console.WriteLine(" Rok:{0}", year.Year);
                    foreach (var month in year.MonthGroup)
                    {
                        Console.WriteLine("    Miesiąc:{0}", month.Key);
                        foreach (var call in month)
                        {
                            Console.WriteLine("      {0} - przez {1} min.",
                                             call.When, call.Duration);
                        }
                    }
                }
            }
        }

        /* Złączenia */

        #region Dane przykładowe

        public class Customer
        {
            public string CustomerID { get; set; }
            public string LastName { get; set; }

            public static List<Customer> SampleData()
            {
                return new List<Customer> {
                    new Customer { CustomerID = "GAJ1", LastName = "Gajewski"},
                    new Customer { CustomerID = "WIE1", LastName = "Wieczorek"},
                    new Customer { CustomerID = "GAD1", LastName = "Gadomski"},
                    new Customer { CustomerID = "DET1", LastName = "Detka"},
                    new Customer { CustomerID = "ZBY1", LastName = "Zbytek"}
                };
            }
        }

        public class Order
        {
            public string CustomerID { get; set; }
            public string OrderNumber { get; set; }


            public static List<Order> SampleData()
            {
                return new List<Order> {
                    new Order { CustomerID = "GAJ1", OrderNumber = "Zamówienie 1"},
                    new Order { CustomerID = "GAJ1", OrderNumber = "Zamówienie 2"},
                    new Order { CustomerID = "DET1", OrderNumber = "Zamówienie 3"},
                    new Order { CustomerID = "DET1", OrderNumber = "Zamówienie 4"},
                    new Order { CustomerID = "ZBY1", OrderNumber = "Zamówienie 5"}
                };
            }
        }

        #endregion

        // prosta funkcja mierząca czas działania
        private long MeasureTime(Action action, int iterations)
        {
            System.Diagnostics.Stopwatch watch = 
                new System.Diagnostics.Stopwatch();
            
            watch.Start();
            
            for (int i = 0; i < iterations; i++)
                action();
            
            return watch.ElapsedMilliseconds;
        }


        [Category("Złączenia")]
        [Title("Listing 4 : Przykład złączenia krzyżowego")]
        [Description("Przykład demonstruje proste złączenie krzyżowe.")]
        public void Listing_4_Cross_Join()
        {
            var outer = Enumerable.Range(1, 3);
            var inner = Enumerable.Range(1, 3);

            // składnia zapytania
            var q = from x in outer
                      from y in inner
                      select new { x, y };

            foreach (var element in q)
                Console.WriteLine("{0}, {1}", element.x, element.y);

            // składnia metody rozszerzenia
            var q1 = outer
                     .SelectMany(
                        x => inner, // selektor kolekcji wewnętrznej
                        (x, y) => new { x, y } // projekcja wybierania
                      );

            Console.WriteLine();

            foreach (var element in q1)
                Console.WriteLine("{0}, {1}", element.x, element.y);
        }

         [Category("Złączenia")]
        [Title("Listing 4-11 : Przykład złączenia krzyżowego sekwencji binarnej")]
        [Description("Przykład demonstruje złączenie krzyżowe budujące wszystkie kombinacje 4-bitowej sekwencji binarnej.")]
        public void Listing_4_11_Cross_Join_BinaryExample()
        {
            var binary = new int[] { 0, 1 };

            var q = from b4 in binary
                    from b3 in binary
                    from b2 in binary
                    from b1 in binary
                    select String.Format(
                        "{0}{1}{2}{3}", b4, b3, b2, b1);

            foreach (var element in q)
                Console.WriteLine(element);
        }

        [Category("Złączenia")]
        [Title("Listing 4 : Bimtampa za pomocą złączenia krzyżowego.")]
        [Description("Przykład demonstruje, jak uzyskać dostęp do każdego piksela bitmapy za pomocą złączenia krzyżowego.")]
        public void Listing_4_Cross_Join_BitmapExample()
        {
            Bitmap bm = new Bitmap(5,5);

            var q = from x in Enumerable.Range(0, bm.Width)
                    from y in Enumerable.Range(0, bm.Height)
                    select bm.GetPixel(x, y);

            foreach (var element in q)
                Console.WriteLine(element);
        }

        [Category("Złączenia")]
        [Title("Listing 4 : Proste złączenie.")]
        [Description("Przykład demonstruje proste złączenie wewnętrzne pomiędzy dwiema tablicami ciągów znaków.")]
        public void Listing_4_Simple_Join()
        {
            string[] outer = new string[] { "a", "b", "c", "d" };
            string[] inner = new string[] { "b", "c", "d", "e" };

            var q = from s1 in outer
                    join s2 in inner on s1 equals s2
                    select string.Format(
                            "Zewnętrzny: {0} Wewnętrzny: {1}", s1, s2);

            foreach (string s in q)
                Console.WriteLine(s);
        }

        [Category("Złączenia")]
        [Title("Listing 4-12 : Złączenie jeden-do-jednego za pomocą składni Join")]
        [Description("Przykład demonstruje złączenie jeden-do-jednego za pomocą składni wyrażenia zapytania.")]
        [LinkedClass("Customer")]
        [LinkedClass("Order")]
        public void Listing_4_12_One_to_One_JoinSyntax()
        {
            // scenariusz: masz numer zamówienia
            // i potrzebujesz danych klienta
            var customers = Customer.SampleData();
            var orders = Order.SampleData();

            var q1 = from o in orders
                     join c in customers on
                         o.CustomerID equals c.CustomerID
                     select new
                     {
                         o.OrderNumber,
                         c.LastName
                     };

            foreach (var order in q1)
                Console.WriteLine(
                    "Klient: {0}  Numer zamówienia: {1}",
                    order.LastName.PadRight(9, ' '),
                    order.OrderNumber);
        }

        [Category("Złączenia")]
        [Title("Listing 4-13 : Złączenie zewnętrzne jeden-do-jednego za pomocą składni Join")]
        [Description("Przykład demonstruje złączenie zewnętrzne jeden-do-jednego za pomocą składni wyrażenia zapytania.")]
        [LinkedClass("Customer")]
        [LinkedClass("Order")]
        public void Listing_4_13_One_to_One_OuterJoinSyntax()
        {
            // scenariusz: masz numer zamówienia
            // i potrzebujesz danych klienta
            var customers = Customer.SampleData();
            var orders = Order.SampleData();


            var q = from c in customers
                    join o in orders on
                        c.CustomerID equals o.CustomerID into j
                    from order in j.DefaultIfEmpty()
                    select new
                    {
                        LastName = c.LastName,
                        Order = order
                    };

            foreach (var element in q)
                Console.WriteLine(
                    "Klient: {0}  Numer zamówienia: {1}",
                    element.LastName.PadRight(9, ' '),
                    element.Order == null ?
                        "(brak zamówień)" : element.Order.OrderNumber);
          

            /* alternatywa, przenieś obsługę wartości null do projekcji wybierania */
            var q1 = from c in customers
                     join o in orders on
                        c.CustomerID equals o.CustomerID into j
                     from order in j.DefaultIfEmpty()
                     select new
                     {
                         LastName = c.LastName,
                         OrderNumber = order == null ?
                               "(brak zamówień)" : order.OrderNumber
                     };
        }

        [Category("Złączenia")]
        [Title("Listing 4 : Złączenie za pomocą klucza złożonego")]
        [Description("Przykład demonstruje złączenie za pomocą klucza złożonego.")]
        public void Listing_4_CompositeKey_Join()
        {
            List<Contact> outer = Contact.SampleData();
            List<CallLog> inner = CallLog.SampleData();

            var q = from contact in outer
                    join call in inner on
                        new { 
                            phone = contact.Phone, 
                            contact.Extension 
                        } 
                    equals 
                        new { 
                            phone = call.Number, 
                            call.Extension 
                        }
                    select new { call, contact };

            foreach (var e in q)
                Console.WriteLine(
                    "{0}, {1}, {2}", 
                    e.call.When, e.call.Number, e.contact.LastName);
        }

        [Category("Złączenia")]
        [Title("Listing 4-14 : Złączenie jeden-do-jednego za pomocą składni podzapytania")]
        [Description("Przykład demonstruje złączenie jeden-do-jednego za pomocą składni podzapytania.")]
        [LinkedClass("Customer")]
        [LinkedClass("Order")]
        public void Listing_4_14_One_to_One_SubQuerySyntax()
        {
            // scenariusz: masz numer zamówienia
            // i potrzebujesz danych klienta
            var customers = Customer.SampleData();
            var orders = Order.SampleData();

            var q2 = from o in orders
                     select new
                     {
                         OrderNumber = o.OrderNumber,
                         LastName = (from c in customers
                                     where c.CustomerID == o.CustomerID
                                     select c.LastName).SingleOrDefault()
                     };

            foreach (var order in q2)
                Console.WriteLine(
                    "Klient: {0}  Numer zamówienia: {1}",
                    order.LastName.PadRight(9, ' '),
                    order.OrderNumber);
        }

        [Category("Złączenia")]
        [Title("Listing 4-15 : Złączenie jeden-do-jednego za pomocą składni SingleOrDefault")]
        [Description("Przykład demonstruje złączenie jeden-do-jednego za pomocą składni SingleOrDefault.")]
        [LinkedClass("Customer")]
        [LinkedClass("Order")]
        public void Listing_4_15_One_to_One_SingleOrDefaultSyntax()
        {
            // scenariusz: masz numer zamówienia
            // i potrzebujesz danych klienta
            var customers = Customer.SampleData();
            var orders = Order.SampleData();

            var q3 = from o in orders
                     let cust = customers
                                .SingleOrDefault(
                                   c => c.CustomerID == o.CustomerID)
                     select new
                     {
                         OrderNumber = o.OrderNumber,
                         LastName = cust.LastName
                     };

            foreach (var order in q3)
                Console.WriteLine(
                    "Klient: {0}  Numer zamówienia: {1}",
                    order.LastName.PadRight(9, ' '),
                    order.OrderNumber);
        }

        [Category("Złączenia")]
        [Title("Listing 4-16 : Złączenie jeden-do-jednego za pomocą składni złączenia krzyżowego/Where")]
        [Description("Przykład demonstruje złączenie jeden-do-jednego za pomocą składni złączenia krzyżowego/Where.")]
        [LinkedClass("Customer")]
        [LinkedClass("Order")]
        public void Listing_4_16_One_to_One_CrossJoinSyntax()
        {
            // scenariusz: masz numer zamówienia
            // i potrzebujesz danych klienta
            var customers = Customer.SampleData();
            var orders = Order.SampleData();

            var q4 = from o in orders
                     from c in customers
                     where c.CustomerID == o.CustomerID
                     select new
                     {
                         o.OrderNumber,
                         c.LastName
                     };

            foreach (var order in q4)
                Console.WriteLine(
                    "Klient: {0}  Numer zamówienia: {1}",
                    order.LastName.PadRight(9, ' '),
                    order.OrderNumber);
        }

        [Category("Złączenia")]
        [Title("Listing 4-12,14,15,16 : Złączenia jeden-do-jednego pomiędzy dwiema kolekcjami")]
        [Description("Przykład demonstruje różne sposoby uzyskiwania złączeń jeden-do-jednego pomiędzy dwiema kolekcjami.")]
        [LinkedClass("Customer")]
        [LinkedClass("Order")]
        public void Listing_4_One_to_One_Comparisons()
        {
            // wykomentuj jedną z dwóch poniższych linii dla testów
            //var loopOuter = 19; // 100 rekordów zewnętrznych
            var loopOuter = 9; // 50 rekordów
            //var loopOuter = 0; // 5 rekordów zewnętrznych

            var customers = Customer.SampleData();
            
            var orders = Order.SampleData();
            for (int i = 0; i < loopOuter; i++)
                orders = orders.Union(Order.SampleData()).ToList();


            // masz numer zamówienia
            // i potrzebujesz danych klienta

            // składnia join
            // najszybsza przy wyszukiwaniu wszystkich rekordów zamówień
            var q1 = from o in orders
                     //where o.OrderNumber == "Order 3"
                     join c in customers on 
                         o.CustomerID equals c.CustomerID
                     select new
                     {
                         o.OrderNumber,
                         c.LastName
                     };

            // składnia podzapytania w wyrażeniu select
            // * jest niezalecana *, użyj operatora SingleOrDefault
            var q2 = from o in orders
                     //where o.OrderNumber == "Order 3"
                     select new
                     {
                        OrderNumber = o.OrderNumber,
                        LastName = (from c in customers
                                    where c.CustomerID == o.CustomerID
                                    select c.LastName).SingleOrDefault()
                     };

            // składnia podzapytania za pomocą operatora SingleOrDefault
            // najszybsza przy wyszukiwaniu pojedynczego rekordu
            var q3 = from o in orders
                     //where o.OrderNumber == "Order 3"
                     let cust = customers
                                .SingleOrDefault(
                                   c => c.CustomerID == o.CustomerID)
                     select new
                     {
                         OrderNumber = o.OrderNumber,
                         LastName = cust.LastName
                     };
            
            // składnia cross-join / where
            // * jest niezalecana *, użyj składni Join lub podzapytania
            var q4 = from o in orders
                     //where o.OrderNumber == "Order 3"
                     from c in customers 
                     where c.CustomerID == o.CustomerID 
                     select new
                     {
                        o.OrderNumber,
                        c.LastName
                     };

            
            Console.WriteLine("q1 = {0}ms, q2 = {1}ms, q3 = {2}ms, q4 = {3}ms",
                MeasureTime(delegate { q1.ToList(); }, 1000000),
                MeasureTime(delegate { q2.ToList(); }, 1000000),
                MeasureTime(delegate { q3.ToList(); }, 1000000),
                MeasureTime(delegate { q4.ToList(); }, 1000000)
                );
             

            Console.WriteLine(Environment.NewLine + "Składnia Join:" );
            foreach (var order in q1)
                Console.WriteLine(
                    "Klient: {0}  Numer zamówienia: {1}",
                    order.LastName.PadRight(9, ' '),
                    order.OrderNumber);

            Console.WriteLine(Environment.NewLine + "Składnia podzapytania:");
            foreach (var order in q2)
                Console.WriteLine(
                    "Klient: {0}  Numer zamówienia: {1}",
                    order.LastName.PadRight(9, ' '),
                    order.OrderNumber);

            Console.WriteLine(Environment.NewLine + "Składnia podzapytania/SingleOrDefault:");
            foreach (var order in q3)
                Console.WriteLine(
                    "Klient: {0}  Numer zamówienia: {1}",
                    order.LastName.PadRight(9, ' '),
                    order.OrderNumber);

            Console.WriteLine(Environment.NewLine + "Złączenia krzyżowe/składania where:");
            foreach (var order in q4)
                Console.WriteLine(
                    "Klient: {0}  Numer zamówienia: {1}",
                    order.LastName.PadRight(9, ' '),
                    order.OrderNumber);

        }

        [Category("Złączenia")]
        [Title("Listing 4-17 : Złączenie jeden-do-wielu za pomocą składni Join/Into")]
        [Description("Przykład demonstruje złączenie jeden-do-wielu za pomocą składni Join/Into.")]
        [LinkedClass("Customer")]
        [LinkedClass("Order")]
        public void Listing_4_17_One_to_Many_JoinIntoSyntax()
        {
            // Scenariusz: masz klienta i potrzebujesz
            // jego zamówień.
            var customers = Customer.SampleData();
            var orders = Order.SampleData();

            var q1 = from c in customers
                     join o in orders on 
                        c.CustomerID equals o.CustomerID
                     into cust_orders
                     select new
                     {
                         LastName = c.LastName,
                         Orders = cust_orders
                     };

            foreach (var customer in q1)
            {
                Console.WriteLine("Nazwisko: {0}", customer.LastName);
                foreach (var order in customer.Orders)
                    Console.WriteLine(" - numer zamówienia: {0}", order.OrderNumber);
            }


            /* Składnia metody rozszerzenia */
            var q1a = customers
                      .GroupJoin(
                           orders,
                           c => c.CustomerID,
                           o => o.CustomerID,
                           (c, o) => new
                           {
                               LastName = c.LastName,
                               Orders = o
                           }
                      );
        }

        /* Sygantura metody rozszerzenia GroupJoin 
        public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>(
            this IEnumerable<TOuter> outer,
            IEnumerable<TInner> inner,
            Func<TOuter, TKey> outerKeySelector,
            Func<TInner, TKey> innerKeySelector,
            Func<TOuter, IEnumerable<TInner>, TResult> resultSelector,
            IEqualityComparer<TKey> comparer // opcjonalny
            )
         */

        [Category("Złączenia")]
        [Title("Listing 4-18 : Złączenie jeden-do-wielu za pomocą składni podzapytania")]
        [Description("Przykład demonstruje złączenie jeden-do-wielu za pomocą składni podzapytania.")]
        [LinkedClass("Customer")]
        [LinkedClass("Order")]
        public void Listing_4_18_One_to_Many_SubQuerySyntax()
        {
            // Scenariusz: masz klienta i potrzebujesz
            // jego zamówień.
            var customers = Customer.SampleData();
            var orders = Order.SampleData();

            var q2 = from c in customers
                     select new
                     {
                         LastName = c.LastName,
                         Orders = from o in orders
                                  where o.CustomerID == c.CustomerID
                                  select o
                     };

            foreach (var customer in q2)
            {
                Console.WriteLine("Nazwisko: {0}", customer.LastName);
                foreach (var order in customer.Orders)
                    Console.WriteLine(" - numer zamówienia: {0}", order.OrderNumber);
            }
        }

        [Category("Złączenia")]
        [Title("Listing 4-19 : Złączenie jeden-do-wielu za pomocą składni operatora ToLookup")]
        [Description("Przykład demonstruje złączenie jeden-do-wielu za pomocą składni operatora ToLookup.")]
        [LinkedClass("Customer")]
        [LinkedClass("Order")]
        public void Listing_4_19_One_to_Many_ToLookupSyntax()
        {
            // Scenariusz: masz klienta i potrzebujesz
            // jego zamówień.
            var customers = Customer.SampleData();
            var orders = Order.SampleData();

            // tworzenie listy wyszukiwania dla sekwencji wewnętrznej
            var orders_lookup = orders.ToLookup(o => o.CustomerID);

            var q3 = from c in customers
                     select new
                     {
                         LastName = c.LastName,
                         Orders = orders_lookup[c.CustomerID]
                     };

            foreach (var customer in q3)
            {
                Console.WriteLine("Nazwisko: {0}", customer.LastName);
                foreach (var order in customer.Orders)
                    Console.WriteLine(" - numer zamówienia: {0}", order.OrderNumber);
            }
        }

        [Category("Złączenia")]
        [Title("Listing 4-17,18,19 : Złączenia jeden-do-wielu pomiędzy dwiema kolekcjami")]
        [Description("Przykład demonstruje różne sposoby tworzenia złączeń jeden-do-wielu pomiędzy dwiema kolekcjami oraz porównuje ich wydajność.")]
        [LinkedClass("Customer")]
        [LinkedClass("Order")]
        public void Listing_4_One_to_Many_Performance()
        {
            // wykomentuj jedną z dwóch poniższych linii dla testów
            // var loopOuter = 19; // 100 rekordów zewnętrznych
            // var loopOuter = 9; // 50 rekordów
            var loopOuter = 0; // 5 rekordów zewnętrznych
            
            var customers = Customer.SampleData();
            for (int i = 0; i < loopOuter; i++)
                customers = customers.Union(Customer.SampleData()).ToList();

            var orders = Order.SampleData();

            // Masz klienta i potrzebujesz
            // jego zamówień.

            // Składnia GroupJoin lub "join into"
            var q1 = from c in customers
                     //where c.CustomerID == "DEA1"
                     join o in orders on c.CustomerID equals o.CustomerID 
                     into cust_orders
                     select new
                     {
                         LastName = c.LastName,
                         Orders = cust_orders
                     };

            var q1a = customers
                       //.Where(c => c.CustomerID == "DEA1")
                       .GroupJoin(
                            orders,
                            c => c.CustomerID,
                            o => o.CustomerID,
                            (c, o) => new { 
                                LastName = c.LastName, 
                                Orders = o }
                       );
           
 
            // przykład złączenia wewnętrznego, wyłącza klientów bez zamówień
            // pytanie: dlaczego wykonanie trwa aż 14 sekund?

            var q1b = from c in customers
                     //where c.CustomerID == "DEA1"
                     join o in orders on c.CustomerID equals o.CustomerID
                     into cust_orders
                     where cust_orders.Any()
                     select new
                     {
                         LastName = c.LastName,
                         Orders = cust_orders
                     };


            // składnia podzapytania
            var q2 = from c in customers
                     //where c.CustomerID == "DEA1"
                     select new
                     {
                         LastName = c.LastName,
                         Orders = from o in orders 
                                  where o.CustomerID == c.CustomerID
                                  select o
                     };

            // składnia ToLookup
            var orders_lookup = orders.ToLookup(o => o.CustomerID);

            var q3 = from c in customers
                     //where c.CustomerID == "DEA1"
                     select new
                     {
                         LastName = c.LastName,
                         Orders = orders_lookup[c.CustomerID]
                     };


            var q1p = from c in customers
                     //where c.CustomerID == "DEA1"
                     join o in orders.AsParallel() on c.CustomerID equals o.CustomerID
                     into cust_orders
                     select new
                     {
                         LastName = c.LastName,
                         Orders = cust_orders
                     };
            
            Console.WriteLine("q1 = {0}ms, q2 = {1}ms, q3 = {2}ms, q1p = {3}ms",
                MeasureTime(delegate { q1.ToList(); }, 1000000),
                MeasureTime(delegate { q2.ToList(); }, 1000000),
                MeasureTime(delegate { q3.ToList(); }, 1000000),
                MeasureTime(delegate { q1p.ToList(); }, 1000000)
                );


            Console.WriteLine(Environment.NewLine + "Składnia złączenia:");
            foreach (var customer in q1)
            {
                Console.WriteLine("Nazwisko: {0}", customer.LastName);
                foreach (var order in customer.Orders)
                    Console.WriteLine(" - Numer zamówienia: {0}", order.OrderNumber);
            }

            Console.WriteLine(Environment.NewLine + "Składnia podzapytania:");
            foreach (var customer in q2)
            {
                Console.WriteLine("Nazwisko: {0}", customer.LastName);
                foreach (var order in customer.Orders)
                    Console.WriteLine(" - Numer zamówienia: {0}", order.OrderNumber);
            }

            Console.WriteLine(Environment.NewLine + "Składnia ToLookup:");
            foreach (var customer in q3)
            {
                Console.WriteLine("Nazwisko: {0}", customer.LastName);
                foreach (var order in customer.Orders)
                    Console.WriteLine(" - Numer zamówienia: {0}", order.OrderNumber);
            }


            Console.WriteLine(Environment.NewLine + "Składnia równoległa:");
            foreach (var customer in q1p)
            {
                Console.WriteLine("Nazwisko: {0}", customer.LastName);
                foreach (var order in customer.Orders)
                    Console.WriteLine(" - Numer zamówienia: {0}", order.OrderNumber);
            }
        }
    }
}