string ومعانيها - لماذا تعامل الرموز التعبيرية مثل stran بطريقة غريبة في سلاسل Swift؟




للنسخ للوحة (5)

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

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

لذلك من المثير للاهتمام للغاية ترميز ؛ الهدف المثالي لاختبار وحدة. ومع ذلك ، يبدو أن Swift لا يعرف كيفية التعامل معها. إليك ما أعنيه:

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

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

لا يتغير هذا إذا كنت أستخدم شيئًا مثل "👩".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! .


Answers

وتناقش الإجابات الأخرى ما يقوم به Swift ، ولكن لا تدخل في تفاصيل كثيرة حول السبب.

هل تتوقع "Å" أن تساوي "Å"؟ أتوقع أنك ستفعل

واحدة من هذه هي رسالة مع الموحد ، والآخر هو شخصية واحدة تتألف. يمكنك إضافة العديد من المكوّنات المختلفة إلى حرف أساسي ، وسيظل الإنسان يعتبره حرفًا واحدًا. للتعامل مع هذا النوع من التناقض ، تم إنشاء مفهوم الحرف الرسومي لتمثيل ما قد يعتبره الإنسان شخصية بغض النظر عن نقاط الترميز المستخدمة.

الآن تم دمج خدمات الرسائل النصية بين الرموز في الرموز التعبيرية الرسومية لسنوات :)🙂 . تمت إضافة رموز تعبيرية مختلفة إلى Unicode.
بدأت هذه الخدمات أيضًا في دمج الرموز التعبيرية معًا في شكل رموز تعبيرية.
لا توجد بالطبع طريقة معقولة لترميز جميع التوليفات الممكنة في نقاط codepoints فردية ، لذا قرر اتحاد Unicode التوسع في مفهوم حروف الصور لتضم هذه الشخصيات المركبة.

ما يجب أن "👩‍👩‍👧‍👦" هذا هو "👩‍👩‍👧‍👦" يجب اعتباره "مجموعة "👩‍👩‍👧‍👦" مفردة إذا كنت تحاول العمل معه على مستوى "👩‍👩‍👧‍👦" ، كما يفعل Swift بشكل افتراضي.

إذا كنت تريد التحقق مما إذا كان يحتوي على "👦" كجزء من ذلك ، فيجب أن تنخفض إلى مستوى أقل.

أنا لا أعرف تركيب سويفت لذا هنا بعض بيرل 6 الذي لديه مستوى دعم مماثل لليونيكود.
(Perl 6 يدعم Unicode version 9 لذا قد يكون هناك تناقض)

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

دعونا نذهب إلى مستوى

# 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

قد يؤدي النزول إلى هذا المستوى إلى جعل بعض الأمور أكثر صعوبة.

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

أفترض أن ذلك في سويفت يجعل ذلك أسهل ، لكن هذا لا يعني أنه لا توجد أشياء أخرى تصبح أكثر صعوبة.

يعمل هذا المستوى على تسهيل عملية تقسيم سلسلة في وسط حرف مركب على سبيل المثال.

ما تسأله عن غير قصد هو لماذا لا يعمل هذا التمثيل الأعلى مستوى مثل تمثيل المستوى الأدنى. الجواب بالطبع ، ليس من المفترض أن.

إذا كنت تسأل نفسك " لماذا يجب أن يكون الأمر معقدًا للغاية " ، فإن الإجابة هي بالطبع " البشر ".


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

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

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

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

الآن ، إذا قمت بالتعديل بين الحروف وطباعتها ، فسوف ترى ما يبدو كأحرف عادية ، لكن في الواقع ، تحتوي الأحرف الثلاثة الأولى على رمز تعبيري بالإضافة إلى أداة وصل صفرية في 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 تتكون من حرف emoji ينتهي بصلة صفر ، وتمريرها إلى أسلوب 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 بإجراء المقارنة على تكافؤ الأحرف مع الأحرف بالضبط . وكملاحظة جانبية ، فإن ما يعنيه الحرف هنا ليس Character Swift ، بل تمثيل 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

يبدو أن شركة Swift تعتبر 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 الموسعة في حرف واحد. على سبيل المثال ، أخذ أحرف الهانغول و و (والتي تتحد لجعل الكلمة الكورية "one": 한 ):

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

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


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

لتبسيط الأمور ، دعنا نتخلص من الأساس ، واستخدم 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", "👦"] . وهذا يجعل كل شيء أكثر وضوحا بعض الشيء. 👩 ليس عضوًا في هذه القائمة (إنها "👩ZWJ") ، ولكن 👦 عضو.

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

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

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

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

(هذا يكرر نقاط بن ليجييرو بشدة. لقد نشرت هذا قبل أن ألاحظ أنه كان قد أجاب. ترك الأمر في حالة أنه أوضح لأي شخص).


فئة تقوم بتعديل سلسلة حالية قابلة للتغيير:

extension String
{
    mutating func replace(originalString:String, withString newString:String)
    {
        let replacedString = self.stringByReplacingOccurrencesOfString(originalString, withString: newString, options: nil, range: nil)
        self = replacedString
    }
}

استعمال:

name.replace(" ", withString: "+")




swift string unicode emoji