Cara Menyortir Daftar <T> berdasarkan properti di objek


1249

Saya memiliki kelas yang disebut Orderyang memiliki properti sepertiOrderId , OrderDate, Quantity, dan Total. Saya punya daftar Orderkelas ini :

List<Order> objListOrder = new List<Order>();
GetOrderList(objListOrder); // fill list of orders

Sekarang saya ingin mengurutkan daftar berdasarkan pada satu properti Orderobjek, misalnya saya perlu mengurutkannya berdasarkan tanggal pesanan atau id pesanan.

Bagaimana saya bisa melakukan ini dalam C #?


9
Tunggu - Anda ingin mengurutkan berdasarkan tanggal pesanan dan nomor pesanan? Karena sebagian besar jawaban tampaknya menganggap bahwa Anda hanya mengurutkan berdasarkan satu atau yang lain.
GalacticCowboy

@ GalacticCowboy, @GenericTypeTea: Pertanyaannya mengatakan "Saya ingin mengurutkan daftar berdasarkan satu properti dari objek Order", tapi saya setuju bahwa di tempat lain tidak sepenuhnya jelas. Either way, itu tidak membuat perbedaan besar apakah Anda perlu mengurutkan berdasarkan satu atau beberapa properti.
LukeH

@ Lukas - Baru saja membaca ulang dan saya setuju. 'Saya ingin mengurutkan daftar berdasarkan satu properti' ... dan 'tanggal pesanan atau id pesanan'. Di antara semua jawaban, kami memiliki semua pangkalan yang dibahas :).
djdd87

Jawaban:


1807

Cara termudah yang bisa saya pikirkan adalah menggunakan Linq:

List<Order> SortedList = objListOrder.OrderBy(o=>o.OrderDate).ToList();

22
bagaimana saya bisa mengurutkan ini dalam urutan menurun.
Cute Bear

229
@BonusKun List <Order> SortedList = objListOrder. OrderByDescending (o => o.OrderDate) .ToList ();
javajavajavajavajava

77
perhatikan bahwa ini membuat daftar baru dengan semua item dalam memori, yang mungkin bermasalah dalam hal kinerja.
staafl

14
@staafl akan listWithObjects = listWithObjects.OrderByDescending(o => o.Status).ToList();cukup untuk usaha seperti itu?
Andrew Grinder

28
@staafl Kami memesan daftar referensi objek, tidak menduplikasi objek itu sendiri sejauh yang saya ketahui. Walaupun ini menggandakan memori yang digunakan oleh daftar referensi itu tidak seburuk menduplikat semua objek itu sendiri sehingga dalam sebagian besar skenario di luar skenario di mana kita berurusan dengan kumpulan data besar dan menahannya dalam memori sudah menjadi masalah, lalu itu sudah cukup.
Lazarus

798

Jika Anda perlu mengurutkan daftar di tempat maka Anda dapat menggunakan Sortmetode ini, melewati Comparison<T>delegasi:

objListOrder.Sort((x, y) => x.OrderDate.CompareTo(y.OrderDate));

Jika Anda lebih suka membuat urutan baru yang diurutkan daripada mengurutkan di tempat maka Anda dapat menggunakan OrderBymetode LINQ , seperti yang disebutkan dalam jawaban lain.


30
Ya, ini adalah jawaban yang "benar" dan harus jauh lebih efisien daripada membuat IEnumerable baru dan kemudian mengubahnya ke daftar baru.
Jonathan Wood

45
Tentu saja, jika Anda memerlukan sortir turun, bertukar xdan ydi sisi kanan panah =>.
Jeppe Stig Nielsen

15
Jawaban sebenarnya yang benar-benar mengurutkan daftar di tempat
Mitchell Currie

3
@PimBrouwers Ini adalah periode opsi yang lebih baik. Sekalipun jumlah memori yang Anda gunakan tidak menjadi masalah, solusi ini menghindari alokasi memori yang tidak perlu yang sangat mahal. Opsi ini sama mudahnya dengan codewise dan urutan besarnya lebih cepat.
Cdaragorn

12
@ JonSchneider Untuk Nullable<>(ambil DateTime?sebagai contoh) Anda dapat menggunakan .Sort((x, y) => Nullable.Compare(x.OrderDate, y.OrderDate))yang akan memperlakukan null sebagai mendahului semua nilai non-nol. Persis sama dengan .Sort((x, y) => Comparer<DateTime?>.Default.Compare(x.OrderDate, y.OrderDate).
Jeppe Stig Nielsen

219

Untuk melakukan ini tanpa LINQ di .Net2.0:

List<Order> objListOrder = GetOrderList();
objListOrder.Sort(
    delegate(Order p1, Order p2)
    {
        return p1.OrderDate.CompareTo(p2.OrderDate);
    }
);

Jika Anda berada di .Net3.0, maka LukeH ini jawabannya adalah yang Anda cari.

Untuk mengurutkan beberapa properti, Anda masih bisa melakukannya di dalam delegasi. Sebagai contoh:

orderList.Sort(
    delegate(Order p1, Order p2)
    {
        int compareDate = p1.Date.CompareTo(p2.Date);
        if (compareDate == 0)
        {
            return p2.OrderID.CompareTo(p1.OrderID);
        }
        return compareDate;
    }
);

Ini akan memberi Anda tanggal naik dengan turun orderIds.

Namun, saya tidak akan merekomendasikan menempel delegasi karena akan berarti banyak tempat tanpa menggunakan kembali kode. Anda harus menerapkan IComparerdan hanya meneruskannya ke Sortmetode Anda . Lihat di sini .

public class MyOrderingClass : IComparer<Order>
{
    public int Compare(Order x, Order y)
    {
        int compareDate = x.Date.CompareTo(y.Date);
        if (compareDate == 0)
        {
            return x.OrderID.CompareTo(y.OrderID);
        }
        return compareDate;
    }
}

Dan kemudian untuk menggunakan kelas IComparer ini, cukup instantiate dan berikan ke metode Sortir Anda:

IComparer<Order> comparer = new MyOrderingClass();
orderList.Sort(comparer);

1
Jawaban luar biasa, dan harus yang benar karena melindungi inisialisasi ulang daftar asli (yang akan selalu dilakukan versi LINQ) memberikan enkapsulasi yang lebih baik.
Jeb

@ tonton, tidak. Idenya adalah untuk dapat memiliki IComparerimplementasi yang berbeda , memberi kita perilaku polimorfik.
radarbob

Bekerja untukku. Saya memposting versi ini dengan opsi pengurutan.
Jack Griffin

109

Cara termudah untuk memesan daftar adalah menggunakan OrderBy

 List<Order> objListOrder = 
    source.OrderBy(order => order.OrderDate).ToList();

Jika Anda ingin memesan dengan beberapa kolom seperti mengikuti SQL Query.

ORDER BY OrderDate, OrderId

Untuk mencapai ini, Anda dapat menggunakan ThenByseperti berikut.

  List<Order> objListOrder = 
    source.OrderBy(order => order.OrderDate).ThenBy(order => order.OrderId).ToList();

32

Melakukannya tanpa Linq seperti yang Anda katakan:

public class Order : IComparable
{
    public DateTime OrderDate { get; set; }
    public int OrderId { get; set; }

    public int CompareTo(object obj)
    {
        Order orderToCompare = obj as Order;
        if (orderToCompare.OrderDate < OrderDate || orderToCompare.OrderId < OrderId)
        {
            return 1;
        }
        if (orderToCompare.OrderDate > OrderDate || orderToCompare.OrderId > OrderId)
        {
            return -1;
        }

        // The orders are equivalent.
        return 0;
    }
}

Kemudian panggil saja .sort () pada daftar Pesanan Anda


3
Seseorang harus terlebih dahulu menguji nullpada aspemain. Yang merupakan inti dari as, karena (ha, ha) (Order)objmelempar pengecualian ketika gagal. if(orderToCompare == null) return 1;.
radarbob

+1 untuk menggunakan antarmuka, yang membuat kode lebih mudah untuk dipelihara dan memperlihatkan kemampuan objek dengan jelas.
AeonOfTime

Jika Bill dan Ted pernah muncul saya akan meminta mereka untuk membawa saya kembali ke 16 Oktober 2014 sehingga saya dapat memperbaiki kesalahan saya di atas - askembali nulljika para pemain gagal. Tapi setidaknya tes nol itu benar.
radarbob

@radarbob yeah .. oops. :) Tapi! Fungsi ini dimaksudkan untuk digunakan secara otomatis dengan mengurutkan daftar List<Order>sehingga jenisnya harus dijamin sesuai pada as2014 sehingga Anda mungkin tidak menulis bug, seperti menghindari pernyataan penjaga yang tidak perlu :)
Jimmy Hoffa

harus dijamin Poin menarik. Jika dienkapsulasi dengan baik sehingga tidak dapat dipanggil kecuali untuk melewati a List<Order>; tetapi Anda dan saya sama-sama telah bertemu dengan programer enkapsulasi diri yang anggapan tak terucapkan adalah "Saya menulis kode ini sehingga tidak akan digunakan salah"
radarbob

26

Solusi Berorientasi Objek Klasik

Pertama-tama saya harus mengarahkan pada kedahsyatan LINQ .... Sekarang kita sudah menyingkir

Variasi jawaban JimmyHoffa. Dengan obat generik, CompareToparameter menjadi aman.

public class Order : IComparable<Order> {

    public int CompareTo( Order that ) {
        if ( that == null ) return 1;
        if ( this.OrderDate > that.OrderDate) return 1;
        if ( this.OrderDate < that.OrderDate) return -1;
        return 0;
    }
}

// in the client code
// assume myOrders is a populated List<Order>
myOrders.Sort(); 

Sortasi default ini tentu saja dapat digunakan kembali. Artinya setiap klien tidak perlu menulis ulang logika sorting secara berlebihan. Mengganti "1" dan "-1" (atau operator logika, pilihan Anda) membalik urutan.


Pendekatan sederhana untuk menyortir objek dalam Daftar. Tapi saya tidak mengerti mengapa Anda mengembalikan 1 jika (itu == null)?
Loc Huynh

4
itu berarti thisobjek lebih besar dari nol. Untuk keperluan pengurutan, referensi objek nol "kurang dari" thisobjek. Itulah cara saya memutuskan untuk menentukan bagaimana null akan mengurutkan.
radarbob

18

// Penyortiran yang benar-benar generik untuk digunakan dengan gridview

public List<T> Sort_List<T>(string sortDirection, string sortExpression, List<T> data)
    {

        List<T> data_sorted = new List<T>();

        if (sortDirection == "Ascending")
        {
            data_sorted = (from n in data
                              orderby GetDynamicSortProperty(n, sortExpression) ascending
                              select n).ToList();
        }
        else if (sortDirection == "Descending")
        {
            data_sorted = (from n in data
                              orderby GetDynamicSortProperty(n, sortExpression) descending
                              select n).ToList();

        }

        return data_sorted;

    }

    public object GetDynamicSortProperty(object item, string propName)
    {
        //Use reflection to get order type
        return item.GetType().GetProperty(propName).GetValue(item, null);
    }

4
Atau, Anda tahu data.OrderBy(),. Agak lebih mudah menemukan kembali roda.
gunr2171

4
Idenya adalah ini akan bekerja dengan semua jenis objek. Sedangkan OrderBy () hanya bekerja dengan objek yang sangat diketik.
roger

1
Terima kasih banyak. Menurut saya, solusi yang paling sederhana dan dapat digunakan menyortir data grid!
kostas ch.

6

Berikut adalah metode ekstensi LINQ umum yang tidak membuat salinan tambahan dari daftar:

public static void Sort<T,U>(this List<T> list, Func<T, U> expression)
    where U : IComparable<U>
{
    list.Sort((x, y) => expression.Invoke(x).CompareTo(expression.Invoke(y)));
}

Untuk menggunakannya:

myList.Sort(x=> x.myProperty);

Baru-baru ini saya membuat tambahan ini yang menerima ICompare<U>, sehingga Anda dapat menyesuaikan perbandingan. Ini berguna ketika saya perlu melakukan semacam string alami:

public static void Sort<T, U>(this List<T> list, Func<T, U> expression, IComparer<U> comparer)
    where U : IComparable<U>
{    
    list.Sort((x, y) => comparer.Compare(expression.Invoke(x), expression.Invoke(y)));
}

Saya sudah menerapkan ini, berfungsi dengan baik. Saya menambahkan parameter "isAscending = true". Untuk mengurutkan dalam urutan menurun, cukup tukar x dan y di dalam dua metode Invoke (). Terima kasih.
Rob L

Jika Anda hanya ingin mengkompilasi ekspresi, Anda mungkin juga hanya menerima delegasi untuk memulai. Selain itu, pendekatan ini memiliki masalah besar jika pemilih menyebabkan efek samping, mahal untuk dihitung, atau tidak deterministik. Penyortiran yang tepat berdasarkan ekspresi yang dipilih perlu memohon pemilih tidak lebih dari sekali per item dalam daftar.
Servy

@Servy - itu semua adalah poin yang valid, tetapi saya akan jujur, saya tidak yakin bagaimana menerapkan beberapa saran Anda (terutama menghentikan pemilih dari menyebabkan efek samping ...?). Saya telah mengubahnya menjadi delegasi. Jika Anda tahu cara membuat beberapa perubahan itu, saya akan senang Anda mengedit kode saya.
Peter

3
@ Peter Anda tidak dapat mencegah delegasi dari menyebabkan efek samping. Apa yang dapat Anda lakukan adalah memastikan mereka tidak pernah disebut lebih dari sekali per objek, ini berarti menghitung nilai untuk setiap objek, menyimpan pasangan objek / diproyeksikan-nilai, dan kemudian menyortir itu . OrderBymelakukan semua itu secara internal.
Servy

Cara lain yang bagus untuk menulis voidmetode ekstensi dalam semangat ini: static class Extensions { public static void SortBy<TSource, TKey>(this List<TSource> self, Func<TSource, TKey> keySelector) { self.SortBy(keySelector, Comparer<TKey>.Default); } public static void SortBy<TSource, TKey>(this List<TSource> self, Func<TSource, TKey> keySelector, IComparer<TKey> comparer) { self.Sort((x, y) => comparer.Compare(keySelector(x), keySelector(y))); } }Dapat ditambah dengan SortByDescendingmetode. Komentar @ Servy berlaku untuk kode saya juga!
Jeppe Stig Nielsen

4

Menggunakan LINQ

objListOrder = GetOrderList()
                   .OrderBy(o => o.OrderDate)
                   .ToList();

objListOrder = GetOrderList()
                   .OrderBy(o => o.OrderId)
                   .ToList();

3
//Get data from database, then sort list by staff name:

List<StaffMember> staffList = staffHandler.GetStaffMembers();

var sortedList = from staffmember in staffList
                 orderby staffmember.Name ascending
                 select staffmember;

3

Peningkatan versi Roger.

Masalah dengan GetDynamicSortProperty adalah bahwa hanya mendapatkan nama properti tetapi apa yang terjadi jika di GridView kita menggunakan NavigationProperties? itu akan mengirim pengecualian, karena ia menemukan nol.

Contoh:

"Employee.Company.Name;" akan macet ... karena hanya mengizinkan "Nama" sebagai parameter untuk mendapatkan nilainya.

Berikut adalah versi yang disempurnakan yang memungkinkan kami untuk mengurutkan berdasarkan Properti Navigasi.

public object GetDynamicSortProperty(object item, string propName)
    {
        try
        {                 
            string[] prop = propName.Split('.'); 

            //Use reflection to get order type                   
            int i = 0;                    
            while (i < prop.Count())
            {
                item = item.GetType().GetProperty(prop[i]).GetValue(item, null);
                i++;
            }                     

            return item;
        }
        catch (Exception ex)
        {
            throw ex;
        }


    } 

3

Anda dapat melakukan sesuatu yang lebih umum tentang pemilihan properti namun spesifik tentang jenis yang Anda pilih, dalam kasus Anda 'Pesan':

tulis fungsi Anda sebagai fungsi umum:

public List<Order> GetOrderList<T>(IEnumerable<Order> orders, Func<Order, T> propertySelector)
        {
            return (from order in orders
                    orderby propertySelector(order)
                    select order).ToList();
        } 

dan kemudian gunakan seperti ini:

var ordersOrderedByDate = GetOrderList(orders, x => x.OrderDate);

Anda bisa menjadi lebih generik dan mendefinisikan tipe terbuka untuk apa yang ingin Anda pesan:

public List<T> OrderBy<T,P>(IEnumerable<T> collection, Func<T,P> propertySelector)
        {
            return (from item in collection
                    orderby propertySelector(item)
                    select item).ToList();
        } 

dan gunakan dengan cara yang sama:

var ordersOrderedByDate = OrderBy(orders, x => x.OrderDate);

Yang merupakan cara kompleks bodoh yang tidak perlu dalam melakukan gaya LINQ 'OrderBy', Tapi itu mungkin memberi Anda petunjuk tentang bagaimana hal itu dapat diterapkan dengan cara yang umum


3

Tolong izinkan saya menyelesaikan jawaban oleh @LukeH dengan beberapa kode sampel, karena saya telah mengujinya, saya yakin ini mungkin berguna untuk beberapa:

public class Order
{
    public string OrderId { get; set; }
    public DateTime OrderDate { get; set; }
    public int Quantity { get; set; }
    public int Total { get; set; }

    public Order(string orderId, DateTime orderDate, int quantity, int total)
    {
        OrderId = orderId;
        OrderDate = orderDate;
        Quantity = quantity;
        Total = total;
    }
}

public void SampleDataAndTest()
{
    List<Order> objListOrder = new List<Order>();

    objListOrder.Add(new Order("tu me paulo ", Convert.ToDateTime("01/06/2016"), 1, 44));
    objListOrder.Add(new Order("ante laudabas", Convert.ToDateTime("02/05/2016"), 2, 55));
    objListOrder.Add(new Order("ad ordinem ", Convert.ToDateTime("03/04/2016"), 5, 66));
    objListOrder.Add(new Order("collocationem ", Convert.ToDateTime("04/03/2016"), 9, 77));
    objListOrder.Add(new Order("que rerum ac ", Convert.ToDateTime("05/02/2016"), 10, 65));
    objListOrder.Add(new Order("locorum ; cuius", Convert.ToDateTime("06/01/2016"), 1, 343));


    Console.WriteLine("Sort the list by date ascending:");
    objListOrder.Sort((x, y) => x.OrderDate.CompareTo(y.OrderDate));

    foreach (Order o in objListOrder)
        Console.WriteLine("OrderId = " + o.OrderId + " OrderDate = " + o.OrderDate.ToString() + " Quantity = " + o.Quantity + " Total = " + o.Total);

    Console.WriteLine("Sort the list by date descending:");
    objListOrder.Sort((x, y) => y.OrderDate.CompareTo(x.OrderDate));
    foreach (Order o in objListOrder)
        Console.WriteLine("OrderId = " + o.OrderId + " OrderDate = " + o.OrderDate.ToString() + " Quantity = " + o.Quantity + " Total = " + o.Total);

    Console.WriteLine("Sort the list by OrderId ascending:");
    objListOrder.Sort((x, y) => x.OrderId.CompareTo(y.OrderId));
    foreach (Order o in objListOrder)
        Console.WriteLine("OrderId = " + o.OrderId + " OrderDate = " + o.OrderDate.ToString() + " Quantity = " + o.Quantity + " Total = " + o.Total);

    //etc ...
}

3
var obj = db.Items.Where...

var orderBYItemId = obj.OrderByDescending(c => Convert.ToInt32(c.ID));

1

Manfaatkan LiNQ OrderBy

List<Order> objListOrder=new List<Order> ();
    objListOrder=GetOrderList().OrderBy(o=>o.orderid).ToList();

1

Berdasarkan GenericTypeTea 's Comparer:
kami dapat memperoleh lebih banyak fleksibilitas dengan menambahkan flag sorting:

public class MyOrderingClass : IComparer<Order> {  
    public int Compare(Order x, Order y) {  
        int compareDate = x.Date.CompareTo(y.Date);  
        if (compareDate == 0) {  
            int compareOrderId = x.OrderID.CompareTo(y.OrderID);  

            if (OrderIdDescending) {  
                compareOrderId = -compareOrderId;  
            }  
            return compareOrderId;  
        }  

        if (DateDescending) {  
            compareDate = -compareDate;  
        }  
        return compareDate;  
    }  

    public bool DateDescending { get; set; }  
    public bool OrderIdDescending { get; set; }  
}  

Dalam skenario ini, Anda harus instantiate sebagai MyOrderingClass secara eksplisit (bukan IComparer )
untuk mengatur properti penyortirannya:

MyOrderingClass comparer = new MyOrderingClass();  
comparer.DateDescending = ...;  
comparer.OrderIdDescending = ...;  
orderList.Sort(comparer);  

1

Tidak ada jawaban di atas yang cukup umum untuk saya, jadi saya buat yang ini:

var someUserInputStringValue = "propertyNameOfObject i.e. 'Quantity' or 'Date'";
var SortedData = DataToBeSorted
                   .OrderBy(m => m.GetType()
                                  .GetProperties()
                                  .First(n => 
                                      n.Name == someUserInputStringValue)
                   .GetValue(m, null))
                 .ToList();

Hati-hati pada set data besar. Ini kode yang mudah tetapi bisa membuat Anda dalam kesulitan jika koleksi sangat besar dan jenis objek koleksi memiliki sejumlah besar bidang. Run time adalah NxM di mana:

N = # Elemen dalam koleksi

M = # Properti dalam Objek


1

Siapa pun yang bekerja dengan tipe nullable, Valuewajib menggunakan CompareTo.

objListOrder.Sort((x, y) => x.YourNullableType.Value.CompareTo(y.YourNullableType.Value));


0

Dari sudut pandang kinerja, yang terbaik adalah dengan menggunakan daftar yang diurutkan sehingga data diurutkan saat ditambahkan ke hasil. Pendekatan lain membutuhkan setidaknya satu iterasi ekstra pada data dan sebagian besar membuat salinan data sehingga tidak hanya kinerja tetapi penggunaan memori juga akan terpengaruh. Mungkin tidak menjadi masalah dengan beberapa elemen tetapi akan dengan ribuan, terutama dalam layanan di mana banyak permintaan bersamaan dapat melakukan penyortiran pada saat yang sama. Lihatlah System.Collections. namespace Umum dan pilih kelas dengan menyortir bukan Daftar.

Dan hindari implementasi generik menggunakan refleksi bila memungkinkan, ini dapat menyebabkan masalah kinerja juga.


Bagaimana ini bisa lebih berkinerja? Memasukkan data ke daftar yang diurutkan adalah O (n), jadi menambahkan n item adalah O (n ^ 2). Apa yang banyak item konkuren coba tambahkan? Ada situasi ketika Anda memiliki siklus CPU ekstra untuk membakar saat item sedang ditambahkan, tetapi ini bukan solusi umum yang baik.
John La Rooy

0

Misalkan Anda memiliki kode berikut, dalam kode ini, kami memiliki kelas Penumpang dengan beberapa properti yang ingin kami urutkan berdasarkan.

public class Passenger
{
    public string Name { get; }
    public string LastName { get; }
    public string PassportNo { get; }
    public string Nationality { get; }

    public Passenger(string name, string lastName, string passportNo, string nationality)
    {
        this.Name = name;
        this.LastName = lastName;
        this.PassportNo = passportNo;
        this.Nationality = nationality;
    }

    public static int CompareByName(Passenger passenger1, Passenger passenger2)
    {
        return String.Compare(passenger1.Name, passenger2.Name);
    }

    public static int CompareByLastName(Passenger passenger1, Passenger passenger2)
    {
        return String.Compare(passenger1.LastName, passenger2.LastName);
    }

    public static int CompareNationality(Passenger passenger1, Passenger passenger2)
    {
        return String.Compare(passenger1.Nationality, passenger2.Nationality);
    }
}

public class TestPassengerSort
{
    Passenger p1 = new Passenger("Johon", "Floid", "A123456789", "USA");
    Passenger p2 = new Passenger("Jo", "Sina", "A987463215", "UAE");
    Passenger p3 = new Passenger("Ped", "Zoola", "A987855215", "Italy");

    public void SortThem()
    {
        Passenger[] passengers = new Passenger[] { p1, p2, p3 };
        List<Passenger> passengerList = new List<Passenger> { p1, p2, p3 };

        Array.Sort(passengers, Passenger.CompareByName);
        Array.Sort(passengers, Passenger.CompareByLastName);
        Array.Sort(passengers, Passenger.CompareNationality);

        passengerList.Sort(Passenger.CompareByName);
        passengerList.Sort(Passenger.CompareByLastName);
        passengerList.Sort(Passenger.CompareNationality);

    }
}

Jadi, Anda dapat menerapkan struktur pengurutan dengan menggunakan delegasi Komposisi.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.