swift - ومعانيها - رموز تعبيرية للنسخ




لماذا يتم التعامل مع رموز تعبيرية مثل 👩‍👩‍👧‍👦 بغرابة في سلاسل سويفت؟ (4)

Emojis ، مثل معيار يونيكود ، معقدة بشكل خادع. يمكن أن تجعل درجات ألوان البشرة والأجناس والوظائف ومجموعات الأشخاص وتسلسلات النجار ذات العرض الصفري والأعلام (حرفان أحادي الرمز) وتعقيدات أخرى تعبيراً عن رموز تعبيرية. يمكن تمثيل شجرة الكريسماس أو شريحة البيتزا أو كومة من Poop بنقطة شفرة Unicode واحدة. ناهيك أنه عند تقديم رموز تعبيرية جديدة ، هناك تأخير بين دعم iOS وإصدار الرموز التعبيرية. ذلك وحقيقة أن إصدارات مختلفة من iOS تدعم إصدارات مختلفة من معيار يونيكود.

TL، DR. لقد عملت على هذه الميزات وفتحت مكتبة من مصادر وأنا مؤلف JKEmoji للمساعدة في تحليل السلاسل مع الرموز التعبيرية. يجعل تحليل سهلة مثل:

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

5

يتم ذلك عن طريق تحديث قاعدة بيانات محلية بشكل روتيني لجميع الرموز التعبيرية المعترف بها اعتبارًا من أحدث إصدار من الشفرة ( 12.0 اعتبارًا من الآونة الأخيرة) وإحالة المرجع إليها باستخدام ما يُعرف برموز تعبيرية صالحة في إصدار نظام التشغيل قيد التشغيل من خلال النظر في تمثيل الصورة النقطية لـ شخصية رمز تعبيري غير معروف.

ملحوظة

تم حذف إجابة سابقة للإعلان عن مكتبتي دون الإشارة بوضوح إلى أنني المؤلف. أنا أقر بهذا مرة أخرى.

يتم ترميز الشخصية 👩‍👩‍👧‍👦 (العائلة التي تضم امرأتين وفتاة وصبي واحد) على هذا النحو:

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

لذلك هو مشفر جدا للاهتمام. الهدف المثالي لاختبار وحدة. ومع ذلك ، لا يبدو أن سويفت يعرف كيفية التعامل معه. إليك ما أعنيه:

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

لذلك ، يقول سويفت أنه يحتوي على نفسه (جيد) وصبي (جيد!). لكنه يقول بعد ذلك أنه لا يحتوي على امرأة أو فتاة أو نجار ذو عرض صفري. ماذا يحصل هنا؟ لماذا تعرف سويفت أنها تحتوي على صبي ولكن ليس امرأة أو فتاة؟ يمكن أن أفهم ما إذا كانت تعاملها كحرف واحد ، ولم أدركها إلا أنها تحتوي على نفسها ، ولكن حقيقة أنها حصلت على عنصر فرعي واحد ولا يحيرني آخرون.

لا يتغير هذا إذا استخدمت شيئًا مثل "👩".characters.first! .

الأمر الأكثر إرباكاً هو:

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

على الرغم من أنني وضعت ZWJs هناك ، إلا أنها لا تنعكس في مجموعة الأحرف. ما يلي كان يقول قليلا:

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

لذا ، أحصل على نفس السلوك مع صفيف الأحرف ... وهو أمر مزعج للغاية ، حيث أنني أعرف كيف يبدو الصفيف.

هذا أيضًا لا يتغير إذا استخدمت شيئًا مثل "👩".characters.first! .


المشكلة الأولى هي أنك تصل إلى Foundation مع contains (Swift's String ليست Collection ) ، لذلك هذا هو سلوك NSString ، الذي لا أعتقد أن المقابض مكونة رموز تعبيرية بقوة مثل Swift. ومع ذلك ، أعتقد أن تطبيق سويفت 8 يستخدم Unicode 8 الآن ، والذي كان يحتاج أيضًا إلى مراجعة حول هذا الموقف في Unicode 10 (لذلك قد يتغير كل هذا عندما يطبق Unicode 10 ؛ لم أتعمق فيما إذا كان سيتم ذلك أم لا).

لتبسيط الأشياء ، دعنا نتخلص من Foundation ، ونستخدم Swift ، والذي يوفر طرق عرض أكثر وضوحًا. سنبدأ بالشخصيات:

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

حسنا. هذا ما توقعناه. لكنها كذبة. دعونا نرى ما هي تلك الشخصيات حقا.

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

آه ... لذلك ["👩ZWJ", "👩ZWJ", "👧ZWJ", "👦"] . هذا يجعل كل شيء أكثر وضوحًا. 👩 ليس عضوًا في هذه القائمة (إنه "WZWJ") ، ولكن 👦 عضو.

المشكلة هي أن Character هو "كتلة كرمية" ، والتي تتكون الأشياء معًا (مثل إرفاق ZWJ). ما كنت تبحث عنه حقا هو عدد قياسي يونيكود. وهذا يعمل بالضبط كما تتوقع:

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

وبالطبع يمكننا أيضًا البحث عن الشخصية الفعلية الموجودة هناك:

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

(يؤدي هذا إلى تكرار نقاط Ben Leggiero بشكل كبير. لقد نشرت هذا قبل أن ألاحظ أنه أجاب. ترك في حال كان أكثر وضوحًا لأي شخص.)


يبدو أن Swift تعتبر ZWJ بمثابة ZWJ ممتدة ذات طابع يسبقها مباشرة. يمكننا أن نرى هذا عند تعيين مجموعة من الأحرف إلى unicodeScalars الخاصة بهم:

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

هذا يطبع ما يلي من 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}"

بالإضافة إلى ذلك ، .contains مجموعات .contains بتوسيع مجموعات الكتل في حرف واحد. على سبيل المثال ، أخذ أحرف الهانغول و و (والتي تتحد لتجعل الكلمة الكورية لكلمة "واحد": 한 ):

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

تعذر العثور على هذا لأن الثلاث مجمعة في مجموعة واحدة تعمل كحرف واحد. وبالمثل ، \u{1F469}\u{200D} ( WOMAN ZWJ ) هي مجموعة واحدة ، تعمل كحرف واحد.


يتعلق هذا بكيفية عمل نوع String في Swift ، وكيفية عمل طريقة contains(_:) .

"👩‍👩‍👧‍👦" هو ما يُعرف بتسلسل الرموز التعبيرية ، والذي يتم عرضه كحرف مرئي واحد في السلسلة. يتكون التسلسل من كائنات Character ، وفي نفس الوقت يتكون من كائنات UnicodeScalar .

إذا قمت بفحص عدد الأحرف في السلسلة ، فسترى أنها مكونة من أربعة أحرف ، بينما إذا قمت بفحص عدد الأرقام في Unicode ، فستظهر لك نتيجة مختلفة:

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

الآن ، إذا قمت بتحليل الأحرف وطباعتها ، فسترى ما يبدو أنه أحرف عادية ، ولكن في الحقيقة تحتوي الأحرف الثلاثة الأولى على كل من الرموز التعبيرية وكذلك UnicodeScalarView ذو عرض صفري في 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"]

كما ترى ، فإن الحرف الأخير فقط لا يحتوي على رابط ذو عرض صفري ، لذلك عند استخدام الأسلوب contains(_:) ، يعمل كما تتوقع. نظرًا لأنك لا تقارن مقابل الرموز التعبيرية التي تحتوي على وصلات ذات عرض صفري ، فلن تجد الطريقة تطابقًا مع أي حرف آخر.

للتوسع في هذا ، إذا قمت بإنشاء String مكونة من حرف رموز تعبيرية تنتهي بوصل عرض صفري ، وتمريرها إلى الأسلوب contains(_:) ، فسيتم تقييمها أيضًا على " false . هذا له علاقة بـ contains(_:) كونه بالضبط نفس range(of:) != nil ، والذي يحاول إيجاد تطابق تام مع الوسيطة المحددة. نظرًا لأن الأحرف التي تنتهي بملحق صفري العرض تشكل تسلسلاً غير مكتمل ، فإن الطريقة تحاول إيجاد تطابق للوسيطة بينما تجمع الأحرف التي تنتهي مع صلات ذات عرض صفري في تسلسل كامل. هذا يعني أن الطريقة لن تجد أي تطابق على الإطلاق إذا:

  1. تنتهي الوسيطة مع نجار ذات عرض صفري و
  2. لا تحتوي السلسلة المراد تحليلها على تسلسل غير مكتمل (أي تنتهي بملحق عرض صفري ولا يتبعها حرف متوافق).

لتوضيح:

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

ومع ذلك ، نظرًا لأن المقارنة تتطلع إلى الأمام فقط ، يمكنك العثور على عدة تسلسلات كاملة أخرى داخل السلسلة من خلال العمل للخلف:

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

سيكون الحل الأسهل هو توفير خيار مقارنة محدد range(of:options:range:locale:) method. يقوم الخيار String.CompareOptions.literal بإجراء المقارنة على معادلة دقيقة لكل حرف . كملاحظة جانبية ، ليس المقصود بالشخصية هنا هو Swift Character ، ولكن تمثيل UTF-16 لكل من سلسلة المثيلات والمقارنة - ومع ذلك ، حيث أن String لا تسمح بتشكيل UTF-16 ، فهذا معادل بشكل أساسي لمقارنة يونيكود التمثيل العددي.

أنا هنا أفرطت في طريقة Foundation ، لذلك إذا كنت بحاجة إلى الطريقة الأصلية ، فقم بإعادة تسمية هذا الأسلوب أو أي شيء آخر:

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

الآن تعمل الطريقة كما يجب "مع" مع كل حرف ، حتى مع التسلسلات غير المكتملة:

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




emoji