emojier: Use consistent emoji order

Most emojis and their variants in the "People" category follow the order
"Person, Man, Woman". But for a few, the order is random.
Also, for the "Select Variant" pop-up feature, Emojier expects variants
with mixed skin tones to be placed near the base emoji or variants with
only one skin tone. Emoji Version 17 adds six emojis that do not follow
this ordering, breaking the feature.

Sort the emojis by this pattern.
This commit is contained in:
Tobias Ozór 2026-08-10 21:33:42 +02:00 committed by Nate Graham
parent e32130d158
commit 5b8c6a28c6
3 changed files with 102 additions and 0 deletions

View file

@ -67,6 +67,13 @@ class EmojierTest(unittest.TestCase):
self.driver.find_element(AppiumBy.NAME, "Recent").click()
self.driver.find_element(AppiumBy.NAME, "grinning face")
def test_3_select_variant(self) -> None:
self.driver.find_element(AppiumBy.NAME, "All").click()
self.driver.find_element(AppiumBy.NAME, "Search").send_keys("handshake")
self.driver.find_element(AppiumBy.NAME, "Select Variant of \"handshake\"").click()
self.driver.find_element(AppiumBy.NAME, "handshake: light skin tone, medium skin tone").click()
time.sleep(1)
self.assertEqual(self.driver.get_clipboard_text(), "🫱🏻‍🫲🏽")
if __name__ == '__main__':
unittest.main()

Binary file not shown.

View file

@ -24,6 +24,21 @@ CLDR_URL = f"https://unicode.org/Public/cldr/{CLDR_VERSION}/{CLDR_FILE}"
CLDR_ANNOTATIONS_DIR = "common/annotations"
CLDR_ANNOTATIONS_DERIVED_DIR = "common/annotationsDerived"
SKIN_TONES = {
b"light skin tone",
b"medium-light skin tone",
b"medium skin tone",
b"medium-dark skin tone",
b"dark skin tone",
}
GENDER_SYMBOLS = {
"\u200D♂️",
"👨",
"\u200D♀️",
"👩",
}
class EmojiAnnotation(object):
def __init__(self):
self.description = ""
@ -35,6 +50,42 @@ class EmojiParser(object):
self.categoryNames = []
self.emojis = collections.OrderedDict()
self.descriptionEmojiMap = {}
self.skinToneMap = collections.defaultdict(list)
self.genderMap = collections.defaultdict(lambda: [None, None, None])
self.genderAndSkinToneVariants = set()
def addToGenderMap(self, emoji):
isMale = "👨" in emoji or "\u200D♂️" in emoji
isFemale = "👩" in emoji or "\u200D♀️" in emoji
if isMale and isFemale:
genderIndex = 0
elif isMale:
genderIndex = 1
else:
genderIndex = 2
neutralEmoji = emoji.replace("\u200D♂️", "")
neutralEmoji = neutralEmoji.replace("👨", "🧑")
neutralEmoji = neutralEmoji.replace("\u200D♀️", "")
neutralEmoji = neutralEmoji.replace("👩", "🧑")
if neutralEmoji not in self.emojis:
return
self.genderMap[neutralEmoji][genderIndex] = emoji
self.genderAndSkinToneVariants.add(emoji)
def getSkinToneVariantsDict(self, emoji):
if emoji is None:
return {}
skinToneVariants = {emoji: self.emojis[emoji]}
for variant in self.skinToneMap[emoji]:
skinToneVariants[variant] = self.emojis[variant]
return skinToneVariants
def parseEmojiTest(self, emojiTestData):
descriptionMapping = dict()
currentGroup = 0;
@ -69,6 +120,11 @@ class EmojiParser(object):
if status == b"fully-qualified":
self.emojis[emoji] = currentGroup
descriptionMapping[description] = emoji
# description format: "version emoji name: modifiers"
versionNameDelimiter = description.index(b" ")
descriptionWithoutVersion = description[versionNameDelimiter+1:]
self.descriptionEmojiMap[descriptionWithoutVersion] = emoji
else:
fullyQualified = descriptionMapping.get(description, None);
if fullyQualified:
@ -76,6 +132,45 @@ class EmojiParser(object):
except e:
pass
for description, emoji in self.descriptionEmojiMap.items():
# description format: "emoji name: modifier 1, modifier 2, ..., modifier n"
nameAndModifiers = description.split(b": ")
if len(nameAndModifiers) == 2:
name, modifiers = nameAndModifiers
if any(skinTone in modifiers for skinTone in SKIN_TONES):
modifiers = modifiers.split(b", ")
modifiersWithoutSkinTone = b", ".join(mod for mod in modifiers if mod not in SKIN_TONES and mod != b"person")
descriptionWithoutSkinTone = name + (b": " + modifiersWithoutSkinTone if modifiersWithoutSkinTone else b"")
try:
neutralEmoji = self.descriptionEmojiMap[descriptionWithoutSkinTone]
self.skinToneMap[neutralEmoji].append(emoji)
self.genderAndSkinToneVariants.add(emoji)
except:
pass
elif any(gender in emoji for gender in GENDER_SYMBOLS):
self.addToGenderMap(emoji)
elif any(gender in emoji for gender in GENDER_SYMBOLS):
self.addToGenderMap(emoji)
self.genderMap["🧑‍🤝‍🧑"] = ["👫", "👬", "👭"]
self.genderAndSkinToneVariants.update(["👫", "👬", "👭"])
sortedEmojis = {}
for emoji, group in self.emojis.items():
if emoji in self.genderAndSkinToneVariants:
pass
elif emoji in self.genderMap:
for genderEmoji in [emoji] + self.genderMap[emoji]:
sortedEmojis.update(self.getSkinToneVariantsDict(genderEmoji))
elif emoji in self.skinToneMap:
sortedEmojis.update(self.getSkinToneVariantsDict(emoji))
else:
sortedEmojis[emoji] = group
self.emojis = sortedEmojis
def parseCldr(self, cldrList):
annotations = dict()
for data in cldrList: