Swift - Mengurutkan berbagai objek dengan beberapa kriteria


92

Saya memiliki array Contactobjek:

var contacts:[Contact] = [Contact]()

Kelas kontak:

Class Contact:NSOBject {
    var firstName:String!
    var lastName:String!
}

Dan saya ingin mengurutkan array itu lastNamekemudian firstNamejika beberapa kontak mendapatkan hal yang sama lastName.

Saya dapat mengurutkan berdasarkan salah satu kriteria tersebut, tetapi tidak keduanya.

contacts.sortInPlace({$0.lastName < $1.lastName})

Bagaimana saya bisa menambahkan lebih banyak kriteria untuk mengurutkan array ini?


2
Lakukan persis seperti yang Anda katakan! Kode Anda di dalam tanda kurung kurawal harus mengatakan: "Jika nama belakangnya sama, urutkan berdasarkan nama depan; jika tidak urutkan berdasarkan nama belakang".
matt

4
Saya melihat beberapa kode berbau di sini: 1) Contactmungkin tidak boleh mewarisi dari NSObject, 2) Contactmungkin harus berupa struct, dan 3) firstNamedan lastNamemungkin tidak boleh secara implisit membuka bungkus opsional.
Alexander - Kembalikan Monica

3
@AMomchilov Tidak ada alasan untuk menyarankan Kontak harus menjadi struct karena Anda tidak tahu apakah sisa kodenya sudah bergantung pada semantik referensi dalam menggunakan contoh itu.
Patrick Goley

3
@AMomchilov "Mungkin" menyesatkan karena Anda tidak tahu persis apa-apa tentang basis kode lainnya. Jika diubah menjadi struct, semua salinan tiba-tiba dihasilkan saat memutasi vars, alih-alih memodifikasi instance yang ada. Ini adalah perubahan perilaku yang drastis dan melakukan hal itu "mungkin" akan menghasilkan bug karena tidak mungkin semuanya telah dikodekan dengan benar untuk semantik referensi dan nilai.
Patrick Goley

1
@AMomchilov Belum mendengar satu alasan mengapa itu mungkin harus sebuah struct. Saya tidak berpikir OP akan menghargai saran yang memodifikasi semantik dari programnya yang lain, terutama ketika itu bahkan tidak perlu untuk menyelesaikan masalah yang ada. Tidak menyadari aturan kompiler legal bagi beberapa ... mungkin saya berada di situs web yang salah
Patrick Goley

Jawaban:


120

Pikirkan tentang arti "mengurutkan menurut beberapa kriteria". Artinya dua objek dibandingkan terlebih dahulu dengan satu kriteria. Kemudian, jika kriteria tersebut sama, maka ikatan akan diputus oleh kriteria selanjutnya, begitu seterusnya hingga mendapatkan urutan yang diinginkan.

let sortedContacts = contacts.sort {
    if $0.lastName != $1.lastName { // first, compare by last names
        return $0.lastName < $1.lastName
    }
    /*  last names are the same, break ties by foo
    else if $0.foo != $1.foo {
        return $0.foo < $1.foo
    }
    ... repeat for all other fields in the sorting
    */
    else { // All other fields are tied, break ties by last name
        return $0.firstName < $1.firstName
    }
}

Apa yang Anda lihat di sini adalah Sequence.sorted(by:)metode , yang berkonsultasi dengan closure yang disediakan untuk menentukan bagaimana elemen dibandingkan.

Jika penyortiran Anda akan digunakan di banyak tempat, mungkin lebih baik membuat jenis Anda sesuai dengan Comparable protokol . Dengan begitu, Anda bisa menggunakan Sequence.sorted()metode , yang berkonsultasi dengan implementasi Comparable.<(_:_:)operator Anda untuk menentukan bagaimana elemen dibandingkan. Dengan cara ini, Anda bisa menyortir setiap Sequencedari Contacts tanpa harus menduplikasi kode penyortiran.


2
The elsetubuh harus antara { ... }lain kode tidak kompilasi.
Luca Angeletti

Mengerti. Saya mencoba menerapkannya tetapi tidak bisa mendapatkan sintaks yang benar. Terima kasih banyak.
sbkl

untuk sortvs. sortInPlacelihat di sini . Seperti yang terlihat di bawah ini , ini jauh lebih modular
Honey

sortInPlaceTIDAK lagi tersedia di Swift 3, Anda harus menggunakannya sort(). sort()akan mengubah array itu sendiri. Juga ada fungsi baru bernama sorted()yang akan mengembalikan array yang diurutkan
Honey

2
@AthanasiusOfAlex Menggunakan ==bukanlah ide yang baik. Ini hanya berfungsi untuk 2 properti. Lebih dari itu, dan Anda mulai mengulangi diri Anda sendiri dengan banyak ekspresi boolean yang rumit
Alexander - Reinstate Monica

123

Menggunakan tupel untuk melakukan perbandingan beberapa kriteria

Cara yang sangat sederhana untuk melakukan pengurutan menurut beberapa kriteria (yaitu mengurutkan berdasarkan satu perbandingan, dan jika setara, kemudian dengan perbandingan lain) adalah dengan menggunakan tupel , karena operator <dan >memiliki kelebihan beban untuk mereka yang melakukan perbandingan leksikografik.

/// Returns a Boolean value indicating whether the first tuple is ordered
/// before the second in a lexicographical ordering.
///
/// Given two tuples `(a1, a2, ..., aN)` and `(b1, b2, ..., bN)`, the first
/// tuple is before the second tuple if and only if
/// `a1 < b1` or (`a1 == b1` and
/// `(a2, ..., aN) < (b2, ..., bN)`).
public func < <A : Comparable, B : Comparable>(lhs: (A, B), rhs: (A, B)) -> Bool

Sebagai contoh:

struct Contact {
  var firstName: String
  var lastName: String
}

var contacts = [
  Contact(firstName: "Leonard", lastName: "Charleson"),
  Contact(firstName: "Michael", lastName: "Webb"),
  Contact(firstName: "Charles", lastName: "Alexson"),
  Contact(firstName: "Michael", lastName: "Elexson"),
  Contact(firstName: "Alex", lastName: "Elexson"),
]

contacts.sort {
  ($0.lastName, $0.firstName) <
    ($1.lastName, $1.firstName)
}

print(contacts)

// [
//   Contact(firstName: "Charles", lastName: "Alexson"),
//   Contact(firstName: "Leonard", lastName: "Charleson"),
//   Contact(firstName: "Alex", lastName: "Elexson"),
//   Contact(firstName: "Michael", lastName: "Elexson"),
//   Contact(firstName: "Michael", lastName: "Webb")
// ]

Ini akan membandingkan properti elemen lastNameterlebih dahulu. Jika tidak sama, maka urutan sortir akan didasarkan pada <perbandingan dengannya. Jika mereka adalah sama, maka akan pindah ke pasangan berikutnya elemen dalam tupel, yaitu membandingkan firstNamesifat.

Pustaka standar menyediakan <dan >membebani tupel dengan 2 hingga 6 elemen.

Jika Anda menginginkan urutan pengurutan yang berbeda untuk properti yang berbeda, Anda cukup menukar elemen di tupel:

contacts.sort {
  ($1.lastName, $0.firstName) <
    ($0.lastName, $1.firstName)
}

// [
//   Contact(firstName: "Michael", lastName: "Webb")
//   Contact(firstName: "Alex", lastName: "Elexson"),
//   Contact(firstName: "Michael", lastName: "Elexson"),
//   Contact(firstName: "Leonard", lastName: "Charleson"),
//   Contact(firstName: "Charles", lastName: "Alexson"),
// ]

Ini sekarang akan mengurutkan berdasarkan lastNameturun, lalu firstNamenaik.


Mendefinisikan sort(by:)kelebihan beban yang membutuhkan banyak predikat

Terinspirasi oleh diskusi tentang Menyortir Koleksi dengan mapclosures dan SortDescriptors , opsi lain adalah menentukan kelebihan beban khusus sort(by:)dan sorted(by:)yang berhubungan dengan beberapa predikat - di mana setiap predikat dipertimbangkan secara bergiliran untuk memutuskan urutan elemen.

extension MutableCollection where Self : RandomAccessCollection {
  mutating func sort(
    by firstPredicate: (Element, Element) -> Bool,
    _ secondPredicate: (Element, Element) -> Bool,
    _ otherPredicates: ((Element, Element) -> Bool)...
  ) {
    sort(by:) { lhs, rhs in
      if firstPredicate(lhs, rhs) { return true }
      if firstPredicate(rhs, lhs) { return false }
      if secondPredicate(lhs, rhs) { return true }
      if secondPredicate(rhs, lhs) { return false }
      for predicate in otherPredicates {
        if predicate(lhs, rhs) { return true }
        if predicate(rhs, lhs) { return false }
      }
      return false
    }
  }
}

extension Sequence {
  mutating func sorted(
    by firstPredicate: (Element, Element) -> Bool,
    _ secondPredicate: (Element, Element) -> Bool,
    _ otherPredicates: ((Element, Element) -> Bool)...
  ) -> [Element] {
    return sorted(by:) { lhs, rhs in
      if firstPredicate(lhs, rhs) { return true }
      if firstPredicate(rhs, lhs) { return false }
      if secondPredicate(lhs, rhs) { return true }
      if secondPredicate(rhs, lhs) { return false }
      for predicate in otherPredicates {
        if predicate(lhs, rhs) { return true }
        if predicate(rhs, lhs) { return false }
      }
      return false
    }
  }
}

( secondPredicate:Parameter ini disayangkan, tetapi diperlukan untuk menghindari membuat ambiguitas dengan sort(by:)kelebihan beban yang ada )

Ini kemudian memungkinkan kita untuk mengatakan (menggunakan contactsarray dari sebelumnya):

contacts.sort(by:
  { $0.lastName > $1.lastName },  // first sort by lastName descending
  { $0.firstName < $1.firstName } // ... then firstName ascending
  // ...
)

print(contacts)

// [
//   Contact(firstName: "Michael", lastName: "Webb")
//   Contact(firstName: "Alex", lastName: "Elexson"),
//   Contact(firstName: "Michael", lastName: "Elexson"),
//   Contact(firstName: "Leonard", lastName: "Charleson"),
//   Contact(firstName: "Charles", lastName: "Alexson"),
// ]

// or with sorted(by:)...
let sortedContacts = contacts.sorted(by:
  { $0.lastName > $1.lastName },  // first sort by lastName descending
  { $0.firstName < $1.firstName } // ... then firstName ascending
  // ...
)

Meskipun situs panggilan tidak sesingkat varian tupel, Anda mendapatkan kejelasan tambahan dengan apa yang dibandingkan dan dalam urutan apa.


Sesuai dengan Comparable

Jika Anda akan melakukan perbandingan semacam ini secara teratur, seperti yang disarankan @AMomchilov & @appzYourLife , Anda dapat menyesuaikan diri Contactdengan Comparable:

extension Contact : Comparable {
  static func == (lhs: Contact, rhs: Contact) -> Bool {
    return (lhs.firstName, lhs.lastName) ==
             (rhs.firstName, rhs.lastName)
  }

  static func < (lhs: Contact, rhs: Contact) -> Bool {
    return (lhs.lastName, lhs.firstName) <
             (rhs.lastName, rhs.firstName)
  }
}

Dan sekarang panggil saja sort()untuk urutan naik:

contacts.sort()

atau sort(by: >)untuk urutan menurun:

contacts.sort(by: >)

Mendefinisikan urutan kustom dalam tipe bertingkat

Jika Anda memiliki susunan urutan lain yang ingin Anda gunakan, Anda dapat menentukannya dalam tipe bertingkat:

extension Contact {
  enum Comparison {
    static let firstLastAscending: (Contact, Contact) -> Bool = {
      return ($0.firstName, $0.lastName) <
               ($1.firstName, $1.lastName)
    }
  }
}

lalu panggil sebagai:

contacts.sort(by: Contact.Comparison.firstLastAscending)

contacts.sort { ($0.lastName, $0.firstName) < ($1.lastName, $1.firstName) } Membantu. Terima kasih
Prabhakar Kasi

Jika seperti saya, sifat akan diurutkan adalah optionals, maka Anda bisa melakukan sesuatu seperti ini: contacts.sort { ($0.lastName ?? "", $0.firstName ?? "") < ($1.lastName ?? "", $1.firstName ?? "") }.
BobCowe

Holly molly! Sangat sederhana namun sangat efisien ... mengapa saya belum pernah mendengarnya ?! Terima kasih banyak!
Ethenyl

@BobCowe Itu membuat Anda pada belas kasihan bagaimana ""dibandingkan dengan string lain (itu datang sebelum string yang tidak kosong). Ini agak tersirat, agak ajaib, dan tidak fleksibel jika Anda ingin kata-kata nilitu muncul di akhir daftar. Saya sarankan Anda melihat nilComparatorfungsi saya stackoverflow.com/a/44808567/3141234
Alexander - Reinstate Monica

19

Pendekatan sederhana lainnya untuk menyortir dengan 2 kriteria ditunjukkan di bawah ini.

Periksa bidang pertama, dalam hal ini lastName, jika tidak sama, urutkan berdasarkan lastName, jika lastNamesama, lalu urutkan menurut bidang kedua, dalam kasus ini firstName.

contacts.sort { $0.lastName == $1.lastName ? $0.firstName < $1.firstName : $0.lastName < $1.lastName  }

Ini memberi lebih banyak fleksibilitas daripada tupel.
Babac

5

Satu hal yang tidak dapat dilakukan oleh pengurutan leksikografis seperti yang dijelaskan oleh @Hamish adalah menangani arah pengurutan yang berbeda, misalnya urutkan berdasarkan kolom pertama yang menurun, kolom berikutnya naik, dll.

Saya membuat posting blog tentang cara melakukannya di Swift 3 dan menjaga kodenya tetap sederhana dan mudah dibaca.

Anda dapat menemukannya di sini:

http://master-method.com/index.php/2016/11/23/sort-a-sequence-ie-arrays-of-objects-by-multiple-properties-in-swift-3/

Anda juga dapat menemukan repositori GitHub dengan kode di sini:

https://github.com/jallauca/SortByMultipleFieldsSwift.playground

Inti dari semuanya, katakanlah, jika Anda memiliki daftar lokasi, Anda akan dapat melakukan ini:

struct Location {
    var city: String
    var county: String
    var state: String
}

var locations: [Location] {
    return [
        Location(city: "Dania Beach", county: "Broward", state: "Florida"),
        Location(city: "Fort Lauderdale", county: "Broward", state: "Florida"),
        Location(city: "Hallandale Beach", county: "Broward", state: "Florida"),
        Location(city: "Delray Beach", county: "Palm Beach", state: "Florida"),
        Location(city: "West Palm Beach", county: "Palm Beach", state: "Florida"),
        Location(city: "Savannah", county: "Chatham", state: "Georgia"),
        Location(city: "Richmond Hill", county: "Bryan", state: "Georgia"),
        Location(city: "St. Marys", county: "Camden", state: "Georgia"),
        Location(city: "Kingsland", county: "Camden", state: "Georgia"),
    ]
}

let sortedLocations =
    locations
        .sorted(by:
            ComparisonResult.flip <<< Location.stateCompare,
            Location.countyCompare,
            Location.cityCompare
        )

1
"Satu hal yang tidak dapat dilakukan oleh jenis leksikografis seperti yang dijelaskan oleh @Hamish adalah menangani arah pengurutan yang berbeda" - ya mereka bisa, cukup tukar elemen di tupel;)
Hamish

Menurut saya ini latihan teoritis yang menarik tetapi jauh lebih rumit daripada jawaban @ Hamish. Lebih sedikit kode adalah kode yang lebih baik menurut saya.
Manuel

5

Pertanyaan ini sudah memiliki banyak jawaban bagus, tetapi saya ingin menunjuk ke sebuah artikel - Urutkan Deskriptor di Swift . Kami memiliki beberapa cara untuk melakukan penyortiran beberapa kriteria.

  1. Menggunakan NSSortDescriptor, cara ini memiliki beberapa keterbatasan, objek harus berupa kelas dan mewarisi dari NSObject.

    class Person: NSObject {
        var first: String
        var last: String
        var yearOfBirth: Int
        init(first: String, last: String, yearOfBirth: Int) {
            self.first = first
            self.last = last
            self.yearOfBirth = yearOfBirth
        }
    
        override var description: String {
            get {
                return "\(self.last) \(self.first) (\(self.yearOfBirth))"
            }
        }
    }
    
    let people = [
        Person(first: "Jo", last: "Smith", yearOfBirth: 1970),
        Person(first: "Joe", last: "Smith", yearOfBirth: 1970),
        Person(first: "Joe", last: "Smyth", yearOfBirth: 1970),
        Person(first: "Joanne", last: "smith", yearOfBirth: 1985),
        Person(first: "Joanne", last: "smith", yearOfBirth: 1970),
        Person(first: "Robert", last: "Jones", yearOfBirth: 1970),
    ]
    

    Di sini, misalnya, kami ingin mengurutkan menurut nama belakang, lalu nama depan, terakhir menurut tahun lahir. Dan kami ingin melakukannya secara tidak peka huruf besar dan menggunakan lokal pengguna.

    let lastDescriptor = NSSortDescriptor(key: "last", ascending: true,
      selector: #selector(NSString.localizedCaseInsensitiveCompare(_:)))
    let firstDescriptor = NSSortDescriptor(key: "first", ascending: true, 
      selector: #selector(NSString.localizedCaseInsensitiveCompare(_:)))
    let yearDescriptor = NSSortDescriptor(key: "yearOfBirth", ascending: true)
    
    
    
    (people as NSArray).sortedArray(using: [lastDescriptor, firstDescriptor, yearDescriptor]) 
    // [Robert Jones (1970), Jo Smith (1970), Joanne smith (1970), Joanne smith (1985), Joe Smith (1970), Joe Smyth (1970)]
    
  2. Menggunakan cara cepat dalam mengurutkan dengan nama belakang / nama depan. Cara ini harus bekerja dengan kedua class / struct. Namun, kami tidak mengurutkan berdasarkan yearOfBirth di sini.

    let sortedPeople = people.sorted { p0, p1 in
        let left =  [p0.last, p0.first]
        let right = [p1.last, p1.first]
    
        return left.lexicographicallyPrecedes(right) {
            $0.localizedCaseInsensitiveCompare($1) == .orderedAscending
        }
    }
    sortedPeople // [Robert Jones (1970), Jo Smith (1970), Joanne smith (1985), Joanne smith (1970), Joe Smith (1970), Joe Smyth (1970)]
    
  3. Cara cepat untuk menggunakan NSSortDescriptor. Ini menggunakan konsep bahwa 'fungsi adalah tipe kelas satu'. SortDescriptor adalah tipe fungsi, mengambil dua nilai, mengembalikan bool. Katakanlah sortByFirstName kita mengambil dua parameter ($ 0, $ 1) dan membandingkan nama depannya. Fungsi gabungan membutuhkan banyak SortDescriptors, membandingkan semuanya dan memberi perintah.

    typealias SortDescriptor<Value> = (Value, Value) -> Bool
    
    let sortByFirstName: SortDescriptor<Person> = {
        $0.first.localizedCaseInsensitiveCompare($1.first) == .orderedAscending
    }
    let sortByYear: SortDescriptor<Person> = { $0.yearOfBirth < $1.yearOfBirth }
    let sortByLastName: SortDescriptor<Person> = {
        $0.last.localizedCaseInsensitiveCompare($1.last) == .orderedAscending
    }
    
    func combine<Value>
        (sortDescriptors: [SortDescriptor<Value>]) -> SortDescriptor<Value> {
        return { lhs, rhs in
            for isOrderedBefore in sortDescriptors {
                if isOrderedBefore(lhs,rhs) { return true }
                if isOrderedBefore(rhs,lhs) { return false }
            }
            return false
        }
    }
    
    let combined: SortDescriptor<Person> = combine(
        sortDescriptors: [sortByLastName,sortByFirstName,sortByYear]
    )
    people.sorted(by: combined)
    // [Robert Jones (1970), Jo Smith (1970), Joanne smith (1970), Joanne smith (1985), Joe Smith (1970), Joe Smyth (1970)]
    

    Ini bagus karena Anda dapat menggunakannya dengan struct dan class, Anda bahkan dapat memperluasnya untuk membandingkan dengan nils.

Tetap saja, membaca artikel asli sangat disarankan. Ini memiliki lebih banyak detail dan dijelaskan dengan baik.


2

Saya akan merekomendasikan menggunakan solusi tupel Hamish karena tidak memerlukan kode tambahan.


Jika Anda menginginkan sesuatu yang berperilaku seperti ifpernyataan tetapi menyederhanakan logika bercabang, Anda dapat menggunakan solusi ini, yang memungkinkan Anda melakukan hal berikut:

animals.sort {
  return comparisons(
    compare($0.family, $1.family, ascending: false),
    compare($0.name, $1.name))
}

Berikut fungsi yang memungkinkan Anda melakukan ini:

func compare<C: Comparable>(_ value1Closure: @autoclosure @escaping () -> C, _ value2Closure: @autoclosure @escaping () -> C, ascending: Bool = true) -> () -> ComparisonResult {
  return {
    let value1 = value1Closure()
    let value2 = value2Closure()
    if value1 == value2 {
      return .orderedSame
    } else if ascending {
      return value1 < value2 ? .orderedAscending : .orderedDescending
    } else {
      return value1 > value2 ? .orderedAscending : .orderedDescending
    }
  }
}

func comparisons(_ comparisons: (() -> ComparisonResult)...) -> Bool {
  for comparison in comparisons {
    switch comparison() {
    case .orderedSame:
      continue // go on to the next property
    case .orderedAscending:
      return true
    case .orderedDescending:
      return false
    }
  }
  return false // all of them were equal
}

Jika Anda ingin mengujinya, Anda dapat menggunakan kode tambahan ini:

enum Family: Int, Comparable {
  case bird
  case cat
  case dog

  var short: String {
    switch self {
    case .bird: return "B"
    case .cat: return "C"
    case .dog: return "D"
    }
  }

  public static func <(lhs: Family, rhs: Family) -> Bool {
    return lhs.rawValue < rhs.rawValue
  }
}

struct Animal: CustomDebugStringConvertible {
  let name: String
  let family: Family

  public var debugDescription: String {
    return "\(name) (\(family.short))"
  }
}

let animals = [
  Animal(name: "Leopard", family: .cat),
  Animal(name: "Wolf", family: .dog),
  Animal(name: "Tiger", family: .cat),
  Animal(name: "Eagle", family: .bird),
  Animal(name: "Cheetah", family: .cat),
  Animal(name: "Hawk", family: .bird),
  Animal(name: "Puma", family: .cat),
  Animal(name: "Dalmatian", family: .dog),
  Animal(name: "Lion", family: .cat),
]

Perbedaan utama dari solusi Jamie adalah bahwa akses ke properti didefinisikan secara inline daripada sebagai metode statis / instance di kelas. Misalnya, $0.familybukan Animal.familyCompare. Dan ascending / descending dikontrol oleh parameter alih-alih operator yang kelebihan beban. Solusi Jamie menambahkan ekstensi pada Array sedangkan solusi saya menggunakan metode sort/ sortedbawaan tetapi membutuhkan dua tambahan untuk didefinisikan: comparedan comparisons.

Demi kelengkapan, berikut perbandingan solusi saya dengan solusi tupel Hamish . Untuk mendemonstrasikan, saya akan menggunakan contoh liar di mana kami ingin mengurutkan orang berdasarkan (name, address, profileViews)solusi Hamish akan mengevaluasi masing-masing dari 6 nilai properti tepat satu kali sebelum perbandingan dimulai. Ini mungkin tidak diinginkan atau mungkin tidak diinginkan. Misalnya, dengan asumsi profileViewspanggilan jaringan mahal, kami mungkin ingin menghindari panggilan profileViewskecuali itu benar-benar diperlukan. Solusi saya akan menghindari evaluasi profileViewssampai $0.name == $1.namedan $0.address == $1.address. Namun, ketika itu benar-benar mengevaluasi profileViewskemungkinan akan mengevaluasi lebih dari sekali.


1

Bagaimana tentang:

contacts.sort() { [$0.last, $0.first].lexicographicalCompare([$1.last, $1.first]) }

lexicographicallyPrecedesmengharuskan semua tipe dalam array menjadi sama. Misalnya [String, String]. Apa yang mungkin diinginkan OP adalah mencampur dan mencocokkan jenis: [String, Int, Bool]sehingga mereka bisa melakukannya [$0.first, $0.age, $0.isActive].
akal

-1

yang berfungsi untuk array [String] saya di Swift 3 dan tampaknya di Swift 4 tidak masalah

array = array.sorted{$0.compare($1, options: .numeric) == .orderedAscending}

Apakah Anda membaca pertanyaan sebelum menjawab? Urutkan berdasarkan beberapa parameter, bukan satu, apa yang Anda sajikan.
Vive
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.