Tại sao các ký tự biểu tượng cảm xúc như được xử lý kỳ lạ trong chuỗi Swift?


540

Ký tự (gia đình có hai phụ nữ, một gái và một trai) được mã hóa như sau:

U+1F469 WOMAN,
‍U+200D ZWJ,
U+1F469 WOMAN,
U+200D ZWJ,
U+1F467 GIRL,
U+200D ZWJ,
U+1F466 BOY

Vì vậy, nó được mã hóa rất thú vị; mục tiêu hoàn hảo cho một bài kiểm tra đơn vị. Tuy nhiên, Swift dường như không biết cách điều trị. Ý tôi là đây:

"👩‍👩‍👧‍👦".contains("👩‍👩‍👧‍👦") // true
"👩‍👩‍👧‍👦".contains("👩") // false
"👩‍👩‍👧‍👦".contains("\u{200D}") // false
"👩‍👩‍👧‍👦".contains("👧") // false
"👩‍👩‍👧‍👦".contains("👦") // true

Vì vậy, Swift nói rằng nó chứa chính nó (tốt) và một cậu bé (tốt!). Nhưng sau đó nó nói rằng nó không chứa một người phụ nữ, cô gái hoặc người tham gia có chiều rộng bằng không. Chuyện gì đang xảy ra ở đây vậy? Tại sao Swift biết nó chứa một bé trai mà không phải là đàn bà hay con gái? Tôi có thể hiểu nếu nó coi nó như một nhân vật duy nhất và chỉ nhận ra nó có chứa chính nó, nhưng thực tế là nó có một thành phần phụ và không có ai khác gây trở ngại cho tôi.

Điều này không thay đổi nếu tôi sử dụng một cái gì đó như "👩".characters.first!.


Điều khó hiểu hơn nữa là đây:

let manual = "\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}"
Array(manual.characters) // ["👩‍", "👩‍", "👧‍", "👦"]

Mặc dù tôi đã đặt ZWJ ở đó, chúng không được phản ánh trong mảng ký tự. Những gì tiếp theo là một chút nói:

manual.contains("👩") // false
manual.contains("👧") // false
manual.contains("👦") // true

Vì vậy, tôi nhận được hành vi tương tự với mảng ký tự ... điều này cực kỳ khó chịu, vì tôi biết mảng đó trông như thế nào.

Điều này cũng không thay đổi nếu tôi sử dụng một cái gì đó như "👩".characters.first!.



1
Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
Martijn Pieters

1
Đã sửa trong Swift 4. "👩‍👩‍👧‍👦".contains("\u{200D}")vẫn trả về false, không chắc đó là lỗi hay tính năng.
Kevin

4
Rất tiếc. Unicode đã hủy hoại văn bản. Nó biến văn bản đơn giản thành một ngôn ngữ đánh dấu.
Boann

6
@Boann có và không ... rất nhiều trong số những thay đổi này đã được đưa vào để thực hiện en / giải mã những thứ như Hangul Jamo (255 mật mã) không phải là một cơn ác mộng tuyệt đối như đối với Kanji (13.108 mật mã) và Ideographs Trung Quốc (199.528 mật mã). Tất nhiên, nó phức tạp và thú vị hơn độ dài của một bình luận SO có thể cho phép, vì vậy tôi khuyến khích bạn tự kiểm tra: D
Ben Leggiero

Câu trả lời:


402

Điều này có liên quan đến cách thức Stringhoạt động của kiểu này trong Swift và cách contains(_:)thức hoạt động của phương thức.

'👩‍👩‍👧‍👦' là thứ được gọi là chuỗi biểu tượng cảm xúc, được hiển thị dưới dạng một ký tự hiển thị trong chuỗi. Trình tự được tạo thành từ Charactercác đối tượng, đồng thời nó được tạo thành từ UnicodeScalarcác đối tượng.

Nếu bạn kiểm tra số lượng ký tự của chuỗi, bạn sẽ thấy rằng nó được tạo thành từ bốn ký tự, trong khi nếu bạn kiểm tra số vô hướng unicode, nó sẽ hiển thị cho bạn một kết quả khác:

print("👩‍👩‍👧‍👦".characters.count)     // 4
print("👩‍👩‍👧‍👦".unicodeScalars.count) // 7

Bây giờ, nếu bạn phân tích các ký tự và in chúng, bạn sẽ thấy những gì trông giống như các ký tự bình thường, nhưng trên thực tế, ba ký tự đầu tiên chứa cả biểu tượng cảm xúc cũng như một phép nối có độ rộng bằng 0 trong chúng UnicodeScalarView:

for char in "👩‍👩‍👧‍👦".characters {
    print(char)

    let scalars = String(char).unicodeScalars.map({ String($0.value, radix: 16) })
    print(scalars)
}

// 👩‍
// ["1f469", "200d"]
// 👩‍
// ["1f469", "200d"]
// 👧‍
// ["1f467", "200d"]
// 👦
// ["1f466"]

Như bạn có thể thấy, chỉ có ký tự cuối cùng không chứa trình nối có độ rộng bằng không, vì vậy khi sử dụng contains(_:)phương thức, nó hoạt động như bạn mong đợi. Vì bạn không so sánh với biểu tượng cảm xúc có chứa các phép nối có độ rộng bằng không, nên phương thức sẽ không tìm thấy kết quả khớp nào cho bất kỳ ngoại trừ ký tự cuối cùng.

Để mở rộng về điều này, nếu bạn tạo một Stringký tự bao gồm một ký tự biểu tượng cảm xúc kết thúc bằng một phép nối có độ rộng bằng 0 và chuyển nó sang contains(_:)phương thức, nó cũng sẽ đánh giá false. Điều này có liên quan đến việc contains(_:)giống hệt như range(of:) != nil, cố gắng tìm một kết quả khớp chính xác với đối số đã cho. Do các ký tự kết thúc bằng một phép nối có độ rộng bằng không tạo thành một chuỗi không hoàn chỉnh, nên phương thức cố gắng tìm một kết quả khớp cho đối số trong khi kết hợp các ký tự kết thúc với các phép nối có độ rộng bằng 0 thành một chuỗi hoàn chỉnh. Điều này có nghĩa là phương thức sẽ không bao giờ tìm thấy kết quả khớp nếu:

  1. đối số kết thúc bằng một phép nối có độ rộng bằng không và
  2. chuỗi phân tích cú pháp không chứa chuỗi không hoàn chỉnh (nghĩa là kết thúc bằng một phép nối có độ rộng bằng 0 và không được theo sau bởi một ký tự tương thích).

Để lam sang tỏ:

let s = "\u{1f469}\u{200d}\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}" // 👩‍👩‍👧‍👦

s.range(of: "\u{1f469}\u{200d}") != nil                            // false
s.range(of: "\u{1f469}\u{200d}\u{1f469}") != nil                   // false

Tuy nhiên, vì so sánh chỉ nhìn về phía trước, bạn có thể tìm thấy một số chuỗi hoàn chỉnh khác trong chuỗi bằng cách làm việc ngược lại:

s.range(of: "\u{1f466}") != nil                                    // true
s.range(of: "\u{1f467}\u{200d}\u{1f466}") != nil                   // true
s.range(of: "\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}") != nil  // true

// Same as the above:
s.contains("\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}")          // true

Giải pháp đơn giản nhất là cung cấp một tùy chọn so sánh cụ thể cho range(of:options:range:locale:)phương thức. Tùy chọn String.CompareOptions.literalthực hiện so sánh trên một tương đương chính xác từng ký tự . Một lưu ý phụ, ý nghĩa của ký tự ở đây không phải là Swift Character, mà là đại diện UTF-16 của cả thể hiện và chuỗi so sánh - tuy nhiên, vì Stringkhông cho phép UTF-16 không đúng định dạng, điều này về cơ bản tương đương với việc so sánh vô hướng Unicode đại diện.

Ở đây tôi đã quá tải Foundationphương thức, vì vậy nếu bạn cần phương thức ban đầu, hãy đổi tên phương thức này hoặc một cái gì đó:

extension String {
    func contains(_ string: String) -> Bool {
        return self.range(of: string, options: String.CompareOptions.literal) != nil
    }
}

Bây giờ phương thức hoạt động như nó "nên" với mỗi ký tự, ngay cả với các chuỗi không hoàn chỉnh:

s.contains("👩")          // true
s.contains("👩\u{200d}")  // true
s.contains("\u{200d}")    // true

47
@MartinR Theo UTR29 hiện tại (Unicode 9.0), đó cụm grapheme mở rộng ( quy tắc GB10 và GB11 ), nhưng Swift rõ ràng sử dụng phiên bản cũ hơn. Rõ ràng sửa lỗi đó là một mục tiêu cho phiên bản 4 của ngôn ngữ , vì vậy hành vi này sẽ thay đổi trong tương lai.
Michael Homer

9
@MichaelHomer: Rõ ràng đã được sửa, "👩‍👩‍👧‍👦".countđánh giá 1với Xcode 9 beta và Swift 4. hiện tại
Martin R

5
Ồ Thật tuyệt vời. Nhưng bây giờ tôi đang hoài cổ về những ngày xưa khi vấn đề tồi tệ nhất mà tôi gặp phải với chuỗi là liệu họ có sử dụng mã hóa theo kiểu C hay Pascal hay không.
Owen Godfrey

2
Tôi hiểu lý do tại sao tiêu chuẩn Unicode có thể cần hỗ trợ điều này, nhưng bạn ơi, đây là một mớ hỗn độn quá mức, nếu có bất cứ điều gì: /
Tái lập lại

110

Vấn đề đầu tiên là bạn đang kết nối với Foundation với contains(Swift Stringkhông phải là một Collection), vì vậy đây là NSStringhành vi, điều mà tôi không tin rằng việc xử lý Emoji được sáng tác mạnh mẽ như Swift. Điều đó nói rằng, Swift tôi tin rằng đang triển khai Unicode 8 ngay bây giờ, cũng cần sửa đổi xung quanh tình huống này trong Unicode 10 (vì vậy tất cả có thể thay đổi khi họ triển khai Unicode 10; Tôi chưa biết liệu nó có hay không).

Để đơn giản hóa mọi thứ, hãy loại bỏ Foundation và sử dụng Swift, cung cấp các khung nhìn rõ ràng hơn. Chúng ta sẽ bắt đầu với các nhân vật:

"👩‍👩‍👧‍👦".characters.forEach { print($0) }
👩‍
👩‍
👧‍
👦

ĐỒNG Ý. Đó là những gì chúng tôi mong đợi. Nhưng đó là một lời nói dối. Hãy xem những nhân vật đó thực sự là gì.

"👩‍👩‍👧‍👦".characters.forEach { print(String($0).unicodeScalars.map{$0}) }
["\u{0001F469}", "\u{200D}"]
["\u{0001F469}", "\u{200D}"]
["\u{0001F467}", "\u{200D}"]
["\u{0001F466}"]

À vậy thì sao ["👩ZWJ", "👩ZWJ", "👧ZWJ", "👦"]. Điều đó làm cho mọi thứ rõ ràng hơn một chút. Không phải là thành viên của danh sách này (đó là "👩ZWJ"), nhưng là thành viên.

Vấn đề là Character"cụm grapheme", kết hợp mọi thứ lại với nhau (như gắn ZWJ). Những gì bạn đang thực sự tìm kiếm là một vô hướng unicode. Và nó hoạt động chính xác như bạn mong đợi:

"👩‍👩‍👧‍👦".unicodeScalars.contains("👩") // true
"👩‍👩‍👧‍👦".unicodeScalars.contains("\u{200D}") // true
"👩‍👩‍👧‍👦".unicodeScalars.contains("👧") // true
"👩‍👩‍👧‍👦".unicodeScalars.contains("👦") // true

Và tất nhiên chúng ta cũng có thể tìm kiếm nhân vật thực sự có trong đó:

"👩‍👩‍👧‍👦".characters.contains("👩\u{200D}") // true

(Điều này trùng lặp rất nhiều điểm của Ben Leggiero. Tôi đã đăng bài này trước khi nhận thấy anh ấy đã trả lời. Để lại trong trường hợp rõ ràng hơn cho bất cứ ai.)


Wth không đại diện ZWJcho?
LinusGeffarth

2
Người tham gia không chiều rộng
Rob Napier

@RobNapier trong Swift 4, Stringđược cho là đã thay đổi thành loại bộ sưu tập. Điều đó có ảnh hưởng đến câu trả lời của bạn không?
Ben Leggiero

Không. Điều đó chỉ thay đổi những thứ như đăng ký. Nó không thay đổi cách các nhân vật hoạt động.
Rob Napier

75

Có vẻ như Swift coi đó ZWJlà một cụm grapheme mở rộng với ký tự ngay trước nó. Chúng ta có thể thấy điều này khi ánh xạ mảng các ký tự vào unicodeScalars:

Array(manual.characters).map { $0.description.unicodeScalars }

Điều này in sau đây từ LLDB:

4 elements
  ▿ 0 : StringUnicodeScalarView("👩‍")
    - 0 : "\u{0001F469}"
    - 1 : "\u{200D}"1 : StringUnicodeScalarView("👩‍")
    - 0 : "\u{0001F469}"
    - 1 : "\u{200D}"2 : StringUnicodeScalarView("👧‍")
    - 0 : "\u{0001F467}"
    - 1 : "\u{200D}"3 : StringUnicodeScalarView("👦")
    - 0 : "\u{0001F466}"

Ngoài ra, .containscác nhóm mở rộng cụm grapheme thành một ký tự. Ví dụ, lấy nhân vật hangul , (trong đó kết hợp để làm cho từ Hàn Quốc cho "một": 한):

"\u{1112}\u{1161}\u{11AB}".contains("\u{1112}") // false

Điều này không thể tìm thấy bởi vì ba điểm mã được nhóm thành một cụm hoạt động như một ký tự. Tương tự, \u{1F469}\u{200D}( WOMAN ZWJ) là một cụm, hoạt động như một ký tự.


19

Các câu trả lời khác thảo luận về những gì Swift làm, nhưng không đi sâu vào chi tiết tại sao.

Bạn có mong đợi những người khác Å Tôi hy vọng bạn sẽ.

Một trong số đó là một lá thư với một cái lược, cái còn lại là một nhân vật sáng tác duy nhất. Bạn có thể thêm nhiều tổ hợp khác nhau vào một nhân vật cơ bản và một người vẫn sẽ coi đó là một nhân vật duy nhất. Để đối phó với sự khác biệt này, khái niệm đồ thị đã được tạo ra để thể hiện những gì con người sẽ coi là một nhân vật bất kể các mật mã được sử dụng.

Bây giờ các dịch vụ nhắn tin văn bản đã kết hợp các ký tự thành biểu tượng cảm xúc đồ họa trong nhiều năm :) →  🙂. Vì vậy, nhiều biểu tượng cảm xúc đã được thêm vào Unicode.
Các dịch vụ này cũng bắt đầu kết hợp biểu tượng cảm xúc với nhau thành biểu tượng cảm xúc tổng hợp.
Tất nhiên không có cách nào hợp lý để mã hóa tất cả các kết hợp có thể thành các điểm mã riêng lẻ, do đó, Hiệp hội Unicode đã quyết định mở rộng khái niệm biểu đồ để bao gồm các ký tự tổng hợp này.

Điều này có nghĩa là "👩‍👩‍👧‍👦"nên được coi là một "cụm grapheme" duy nhất nếu bạn cố gắng làm việc với nó ở cấp grapheme, như Swift làm theo mặc định.

Nếu bạn muốn kiểm tra xem nó có phải "👦"là một phần của điều đó không, thì bạn nên chuyển xuống mức thấp hơn.


Tôi không biết cú pháp Swift nên đây là một số Perl 6 có mức hỗ trợ tương tự cho Unicode.
(Perl 6 hỗ trợ Unicode phiên bản 9 nên có thể có sự khác biệt)

say "\c[family: woman woman girl boy]" eq "👩‍👩‍👧‍👦"; # True

# .contains is a Str method only, in Perl 6
say "👩‍👩‍👧‍👦".contains("👩‍👩‍👧‍👦")    # True
say "👩‍👩‍👧‍👦".contains("👦");        # False
say "👩‍👩‍👧‍👦".contains("\x[200D]");  # False

# comb with no arguments splits a Str into graphemes
my @graphemes = "👩‍👩‍👧‍👦".comb;
say @graphemes.elems;                # 1

Chúng ta hãy đi xuống một cấp độ

# look at it as a list of NFC codepoints
my @components := "👩‍👩‍👧‍👦".NFC;
say @components.elems;                     # 7

say @components.grep("👦".ord).Bool;       # True
say @components.grep("\x[200D]".ord).Bool; # True
say @components.grep(0x200D).Bool;         # True

Đi xuống cấp độ này có thể làm cho một số điều khó khăn hơn mặc dù.

my @match = "👩‍👩‍👧‍👦".ords;
my $l = @match.elems;
say @components.rotor( $l => 1-$l ).grep(@match).Bool; # True

Tôi cho rằng .containstrong Swift làm điều đó dễ dàng hơn, nhưng điều đó không có nghĩa là không có những thứ khác trở nên khó khăn hơn.

Làm việc ở cấp độ này làm cho việc vô tình tách một chuỗi ở giữa một ký tự tổng hợp chẳng hạn.


Những gì bạn đang vô tình hỏi là tại sao đại diện cấp cao hơn này không hoạt động như một đại diện cấp thấp hơn sẽ. Câu trả lời là tất nhiên, nó không phải là.

Nếu bạn đang tự hỏi mình tại sao điều này lại phức tạp như vậy , thì câu trả lời tất nhiên là con người .


4
Bạn đã mất tôi trên dòng ví dụ cuối cùng của bạn; làm gì rotorgreplàm gì ở đây Và là 1-$lgì?
Ben Leggiero

4
Thuật ngữ "grapheme" ít nhất 50 năm tuổi. Unicode đã đưa nó vào tiêu chuẩn bởi vì họ đã sử dụng thuật ngữ "ký tự" để chỉ một cái gì đó hoàn toàn khác với những gì người ta thường nghĩ là một nhân vật. Tôi có thể đọc những gì bạn viết là phù hợp với điều đó nhưng nghi ngờ những người khác có thể có ấn tượng sai, do đó nhận xét này (hy vọng làm rõ).
raiph

2
@BenLeggiero Đầu tiên , rotor. Các mã say (1,2,3,4,5,6).rotor(3)mang lại ((1 2 3) (4 5 6)). Đó là một danh sách các danh sách, mỗi chiều dài 3. say (1,2,3,4,5,6).rotor(3=>-2)mang lại kết quả tương tự ngoại trừ danh sách con thứ hai bắt đầu bằng 2thay vì 4, thứ ba với 3, v.v., mang lại ((1 2 3) (2 3 4) (3 4 5) (4 5 6)). Nếu @matchchứa "👩‍👩‍👧‍👦".ordsthì mã của Brad Brad chỉ tạo một danh sách con, do đó =>1-$lbit không liên quan (không được sử dụng). Nó chỉ liên quan nếu @matchngắn hơn @components.
raiph

1
grepcố gắng khớp từng phần tử trong invocant của nó (trong trường hợp này là danh sách các danh sách con của @components). Nó cố gắng khớp từng phần tử với đối số so khớp của nó (trong trường hợp này @match). Sau .Boolđó trả về Trueiff grepsản xuất ít nhất một trận đấu.
raiph

18

Cập nhật Swift 4.0

Chuỗi đã nhận được rất nhiều sửa đổi trong bản cập nhật Swift 4, như được ghi lại trong SE-0163 . Hai biểu tượng cảm xúc được sử dụng cho bản demo này đại diện cho hai cấu trúc khác nhau. Cả hai đều được kết hợp với một chuỗi biểu tượng cảm xúc.

👍🏽là sự kết hợp của hai biểu tượng cảm xúc 👍🏽

👩‍👩‍👧‍👦là sự kết hợp của bốn biểu tượng cảm xúc, với kết nối có độ rộng bằng không được kết nối. Định dạng là👩‍joiner👩‍joiner👧‍joiner👦

1. Đếm

Trong biểu tượng cảm xúc Swift 4.0 được tính là cụm grapheme. Mỗi biểu tượng cảm xúc được tính là 1. countThuộc tính cũng có sẵn trực tiếp cho chuỗi. Vì vậy, bạn có thể trực tiếp gọi nó như thế này.

"👍🏽".count  // 1. Not available on swift 3
"👩‍👩‍👧‍👦".count  // 1. Not available on swift 3

Mảng ký tự của chuỗi cũng được tính là cụm grapheme trong Swift 4.0, vì vậy cả hai mã sau đây đều in 1. Hai biểu tượng cảm xúc này là ví dụ về chuỗi biểu tượng cảm xúc, trong đó một số biểu tượng cảm xúc được kết hợp với nhau hoặc không có liên kết độ rộng bằng không \u{200d}giữa chúng. Trong swift 3.0, mảng ký tự của chuỗi như vậy tách ra từng biểu tượng cảm xúc và kết quả là một mảng có nhiều phần tử (biểu tượng cảm xúc). Người tham gia được bỏ qua trong quá trình này. Tuy nhiên, trong Swift 4.0, mảng ký tự xem tất cả biểu tượng cảm xúc là một mảnh. Vì vậy, bất kỳ biểu tượng cảm xúc nào sẽ luôn là 1.

"👍🏽".characters.count  // 1. In swift 3, this prints 2
"👩‍👩‍👧‍👦".characters.count  // 1. In swift 3, this prints 4

unicodeScalars vẫn không thay đổi trong Swift 4. Nó cung cấp các ký tự Unicode duy nhất trong chuỗi đã cho.

"👍🏽".unicodeScalars.count  // 2. Combination of two emoji
"👩‍👩‍👧‍👦".unicodeScalars.count  // 7. Combination of four emoji with joiner between them

2. Chứa

Trong Swift 4.0, containsphương thức bỏ qua phép nối độ rộng bằng 0 trong biểu tượng cảm xúc. Vì vậy, nó trả về true cho bất kỳ thành phần nào trong bốn thành phần biểu tượng cảm xúc "👩‍👩‍👧‍👦"và trả về false nếu bạn kiểm tra trình nối. Tuy nhiên, trong Swift 3.0, người tham gia không bị bỏ qua và được kết hợp với biểu tượng cảm xúc ở phía trước. Vì vậy, khi bạn kiểm tra xem "👩‍👩‍👧‍👦"có chứa biểu tượng cảm xúc ba thành phần đầu tiên không, kết quả sẽ là sai

"👍🏽".contains("👍")       // true
"👍🏽".contains("🏽")        // true
"👩‍👩‍👧‍👦".contains("👩‍👩‍👧‍👦")       // true
"👩‍👩‍👧‍👦".contains("👩")       // true. In swift 3, this prints false
"👩‍👩‍👧‍👦".contains("\u{200D}") // false
"👩‍👩‍👧‍👦".contains("👧")       // true. In swift 3, this prints false
"👩‍👩‍👧‍👦".contains("👦")       // true

0

Biểu tượng cảm xúc, giống như tiêu chuẩn unicode, rất phức tạp. Tông màu da, giới tính, công việc, nhóm người, trình tự nối không có độ rộng bằng không, cờ (unicode 2 ký tự) và các biến chứng khác có thể làm cho biểu tượng cảm xúc phân tích lộn xộn. Một cây thông Giáng sinh, một lát bánh pizza hoặc một đống Poop đều có thể được biểu diễn bằng một điểm mã Unicode duy nhất. Chưa kể khi giới thiệu biểu tượng cảm xúc mới, có sự chậm trễ giữa hỗ trợ iOS và phát hành biểu tượng cảm xúc. Điều đó và thực tế là các phiên bản iOS khác nhau hỗ trợ các phiên bản khác nhau của tiêu chuẩn unicode.

TL; DR. Tôi đã làm việc trên các tính năng này và mở ra một thư viện mà tôi là tác giả của JKEmoji để giúp phân tích các chuỗi với biểu tượng cảm xúc. Nó làm cho phân tích cú pháp dễ dàng như:

print("I love these emojis 👩‍👩‍👧‍👦💪🏾🧥👧🏿🌈".emojiCount)

5

Nó thực hiện điều đó bằng cách thường xuyên làm mới cơ sở dữ liệu cục bộ của tất cả các biểu tượng cảm xúc được công nhận là phiên bản unicode mới nhất ( 12.0 tính đến gần đây) và tham chiếu chéo chúng với biểu tượng cảm xúc hợp lệ trong phiên bản HĐH đang chạy bằng cách xem biểu diễn bitmap của một nhân vật biểu tượng cảm xúc không được công nhận.

GHI CHÚ

Một câu trả lời trước đó đã bị xóa để quảng cáo thư viện của tôi mà không nói rõ rằng tôi là tác giả. Tôi thừa nhận điều này một lần nữa.


2
Mặc dù tôi rất ấn tượng với thư viện của bạn và tôi thấy nó thường liên quan đến chủ đề như thế nào, tôi không thấy điều này liên quan trực tiếp đến câu hỏi
Ben Leggiero 16/03/19
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.