sonatoki 0.1.4__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sonatoki/Cleaners.py CHANGED
@@ -23,7 +23,7 @@ class RegexCleaner(Cleaner):
23
23
  return re.sub(cls.pattern, cls.replace, token)
24
24
 
25
25
 
26
- class ConsecutiveDuplicates(RegexCleaner):
26
+ class ConsecutiveDuplicates(Cleaner):
27
27
  """Remove consecutive duplicates from an input string, ignoring case.
28
28
 
29
29
  The first match of any 2+ will become `\\1`, preserving initial case.
@@ -35,8 +35,31 @@ class ConsecutiveDuplicates(RegexCleaner):
35
35
  This may be undesirable for moraic scripts like Hiragana, where `わわ` would be
36
36
  incorrectly reduced to `わ`. This does preserve phonotactic validity, though."""
37
37
 
38
+ @classmethod
39
+ @override
40
+ def clean(cls, token: str) -> str:
41
+ if not token:
42
+ return token
43
+
44
+ output = token[0]
45
+
46
+ last_output = output.lower() # ignore case in comparison
47
+ for i in range(1, len(token)):
48
+ cur_char = token[i].lower()
49
+ if cur_char == last_output:
50
+ continue
51
+ output += token[i] # preserve case of string
52
+ last_output = cur_char
53
+ return output
54
+
55
+
56
+ class ConsecutiveDuplicatesRe(RegexCleaner):
57
+ """Reference implementation for `ConsecutiveDuplicates`."""
58
+
38
59
  pattern = re.compile(r"(.)\1+", flags=re.IGNORECASE)
39
60
  replace = r"\1"
40
61
 
41
62
 
42
- __all__ = ["ConsecutiveDuplicates"]
63
+ __all__ = [
64
+ "ConsecutiveDuplicates",
65
+ ]
sonatoki/Configs.py CHANGED
@@ -21,7 +21,7 @@ from sonatoki.Filters import (
21
21
  )
22
22
  from sonatoki.Scorers import Number, Scorer, PassFail, SoftScaling, SoftPassFail
23
23
  from sonatoki.Cleaners import Cleaner, ConsecutiveDuplicates
24
- from sonatoki.Tokenizers import Tokenizer, WordTokenizerTok
24
+ from sonatoki.Tokenizers import Tokenizer, WordTokenizer
25
25
  from sonatoki.Preprocessors import (
26
26
  URLs,
27
27
  Preprocessor,
@@ -49,7 +49,7 @@ BaseConfig: IloConfig = {
49
49
  "scoring_filters": [],
50
50
  "scorer": PassFail,
51
51
  "passing_score": 0.8,
52
- "word_tokenizer": WordTokenizerTok,
52
+ "word_tokenizer": WordTokenizer,
53
53
  }
54
54
 
55
55
 
@@ -70,11 +70,11 @@ TelegramConfig: IloConfig = deepcopy(PrefConfig)
70
70
  ForumConfig: IloConfig = deepcopy(PrefConfig)
71
71
 
72
72
  __all__ = [
73
- "IloConfig",
74
73
  "BaseConfig",
75
- "PrefConfig",
76
- "LazyConfig",
77
74
  "DiscordConfig",
78
- "TelegramConfig",
79
75
  "ForumConfig",
76
+ "IloConfig",
77
+ "LazyConfig",
78
+ "PrefConfig",
79
+ "TelegramConfig",
80
80
  ]
sonatoki/Filters.py CHANGED
@@ -1,26 +1,30 @@
1
1
  # STL
2
+ import re
2
3
  from abc import ABC, abstractmethod
3
4
  from typing import Set
4
5
  from functools import lru_cache as cache # cache comes in 3.9
5
6
 
6
7
  # PDM
7
- import regex as re
8
+ import regex
8
9
  from typing_extensions import override
9
10
 
10
11
  # LOCAL
11
12
  from sonatoki.constants import (
12
13
  VOWELS,
14
+ NIMI_PU,
15
+ ALPHABET,
16
+ ALLOWABLES,
13
17
  CONSONANTS,
14
- NIMI_PU_SET,
15
- ALPHABET_SET,
16
- ALLOWABLES_SET,
17
- NIMI_LINKU_SET,
18
- NIMI_PU_ALE_SET,
19
- NIMI_LINKU_ALE_SET,
20
- NIMI_LINKU_SANDBOX_SET,
18
+ NIMI_LINKU,
19
+ NIMI_PU_ALE,
20
+ POSIX_PUNCT,
21
+ UNICODE_PUNCT,
22
+ NIMI_LINKU_ALE,
23
+ ALL_PUNCT_RANGES,
24
+ NIMI_LINKU_SANDBOX,
21
25
  )
22
26
 
23
- re.DEFAULT_VERSION = re.VERSION1
27
+ regex.DEFAULT_VERSION = regex.VERSION1
24
28
 
25
29
 
26
30
  class Filter(ABC):
@@ -41,7 +45,17 @@ class RegexFilter(Filter):
41
45
  return not not re.fullmatch(cls.pattern, token)
42
46
 
43
47
 
44
- class SetFilter(Filter):
48
+ class Regex1Filter(Filter):
49
+ pattern: "regex.Pattern[str]"
50
+
51
+ @classmethod
52
+ @override
53
+ @cache(maxsize=None)
54
+ def filter(cls, token: str) -> bool:
55
+ return not not regex.fullmatch(cls.pattern, token)
56
+
57
+
58
+ class MemberFilter(Filter):
45
59
  tokens: Set[str]
46
60
 
47
61
  @classmethod
@@ -51,8 +65,18 @@ class SetFilter(Filter):
51
65
  return token.lower() in cls.tokens
52
66
 
53
67
 
54
- class Miscellaneous(SetFilter):
55
- tokens = ALLOWABLES_SET
68
+ class SubsetFilter(Filter):
69
+ tokens: Set[str]
70
+
71
+ @classmethod
72
+ @override
73
+ @cache(maxsize=None)
74
+ def filter(cls, token: str) -> bool:
75
+ return set(token.lower()).issubset(cls.tokens)
76
+
77
+
78
+ class Miscellaneous(MemberFilter):
79
+ tokens = set(ALLOWABLES)
56
80
 
57
81
 
58
82
  class ProperName(Filter):
@@ -70,26 +94,28 @@ class ProperName(Filter):
70
94
  @cache(maxsize=None)
71
95
  def filter(cls, token: str) -> bool:
72
96
  return token == token.capitalize()
97
+ # TODO: If the token is in a script which doesn't have a case distinction,
98
+ # this will errantly match.
73
99
 
74
100
 
75
- class NimiPu(SetFilter):
76
- tokens = NIMI_PU_SET
101
+ class NimiPu(MemberFilter):
102
+ tokens = set(NIMI_PU)
77
103
 
78
104
 
79
- class NimiPuAle(SetFilter):
80
- tokens = NIMI_PU_ALE_SET
105
+ class NimiPuAle(MemberFilter):
106
+ tokens = set(NIMI_PU_ALE)
81
107
 
82
108
 
83
- class NimiLinku(SetFilter):
84
- tokens = NIMI_LINKU_SET
109
+ class NimiLinku(MemberFilter):
110
+ tokens = set(NIMI_LINKU)
85
111
 
86
112
 
87
- class NimiLinkuAle(SetFilter):
88
- tokens = NIMI_LINKU_ALE_SET
113
+ class NimiLinkuAle(MemberFilter):
114
+ tokens = set(NIMI_LINKU_ALE)
89
115
 
90
116
 
91
- class NimiLinkuSandbox(SetFilter):
92
- tokens = NIMI_LINKU_SANDBOX_SET
117
+ class NimiLinkuSandbox(MemberFilter):
118
+ tokens = set(NIMI_LINKU_SANDBOX)
93
119
 
94
120
 
95
121
  class Phonotactic(RegexFilter):
@@ -122,13 +148,12 @@ class Syllabic(RegexFilter):
122
148
  )
123
149
 
124
150
 
125
- class Alphabetic(Filter):
126
- @classmethod
127
- @override
128
- @cache(maxsize=None)
129
- def filter(cls, token: str) -> bool:
130
- # Faster than regex version
131
- return set(token.lower()).issubset(ALPHABET_SET)
151
+ class Alphabetic(SubsetFilter):
152
+ tokens = set(ALPHABET)
153
+
154
+
155
+ class AlphabeticRe(RegexFilter):
156
+ pattern = re.compile(rf"[{ALPHABET}]+", flags=re.IGNORECASE)
132
157
 
133
158
 
134
159
  class Numeric(Filter):
@@ -147,18 +172,35 @@ class Numeric(Filter):
147
172
  return msg.isnumeric()
148
173
 
149
174
 
150
- class Punctuation(RegexFilter):
151
- pattern = re.compile(r"[\p{Punctuation}\p{posix_punct}]+")
175
+ class Punctuation(SubsetFilter):
176
+ """Identify whether a token is entirely punctuation. Fastest implementation."""
177
+
178
+ tokens = set(POSIX_PUNCT + UNICODE_PUNCT)
179
+
180
+
181
+ class PunctuationRe(RegexFilter):
182
+ """Faster implementation of `PunctuationRe1`.
183
+ Goes out of date compared to the `regex` library if UNICODE_PUNCT is not updated."""
184
+
185
+ pattern = re.compile(rf"[{ALL_PUNCT_RANGES}]+")
186
+
187
+
188
+ class PunctuationRe1(Regex1Filter):
189
+ """Reference implementation for identifying tokens made entirely of punctuation."""
190
+
191
+ pattern = regex.compile(r"[\p{Punctuation}\p{posix_punct}]+")
152
192
 
153
193
 
154
194
  __all__ = [
155
- "NimiPu",
195
+ "Alphabetic",
156
196
  "NimiLinku",
157
197
  "NimiLinkuAle",
198
+ "NimiLinkuSandbox",
199
+ "NimiPu",
200
+ "NimiPuAle",
201
+ "Numeric",
158
202
  "Phonotactic",
159
- "Syllabic",
160
- "Alphabetic",
161
203
  "ProperName",
162
204
  "Punctuation",
163
- "Numeric",
205
+ "Syllabic",
164
206
  ]
sonatoki/Preprocessors.py CHANGED
@@ -17,13 +17,14 @@ It is up to the user to order them appropriately.
17
17
  """
18
18
 
19
19
  # STL
20
+ import re
20
21
  from abc import ABC, abstractmethod
21
22
 
22
23
  # PDM
23
- import regex as re
24
+ import regex
24
25
  from typing_extensions import override
25
26
 
26
- re.DEFAULT_VERSION = re.VERSION1
27
+ regex.DEFAULT_VERSION = regex.VERSION1
27
28
 
28
29
 
29
30
  class Preprocessor(ABC):
@@ -43,6 +44,16 @@ class RegexPreprocessor(Preprocessor):
43
44
  return re.sub(cls.pattern, cls.replace, msg)
44
45
 
45
46
 
47
+ class Regex1Preprocessor(Preprocessor):
48
+ pattern: "regex.Pattern[str]"
49
+ replace: str = " "
50
+
51
+ @classmethod
52
+ @override
53
+ def process(cls, msg: str) -> str:
54
+ return regex.sub(cls.pattern, cls.replace, msg)
55
+
56
+
46
57
  """
47
58
  The following classes are Ignorables.
48
59
 
@@ -146,17 +157,17 @@ class AllQuotes(RegexPreprocessor):
146
157
 
147
158
 
148
159
  __all__ = [
160
+ "AllQuotes",
149
161
  "AngleBracketObject",
162
+ "ArrowQuote",
163
+ "Backticks",
150
164
  "DiscordChannels",
165
+ "DiscordEmotes",
151
166
  "DiscordMentions",
152
167
  "DiscordSpecial",
153
- "DiscordEmotes",
154
- "SingleQuotes",
155
168
  "DoubleQuotes",
156
- "ArrowQuote",
157
- "AllQuotes",
158
- "Backticks",
159
169
  "Reference",
170
+ "SingleQuotes",
160
171
  "Spoilers",
161
172
  "URLs",
162
173
  ]
sonatoki/Scorers.py CHANGED
@@ -1,6 +1,5 @@
1
1
  # STL
2
2
  import math
3
- import logging
4
3
  from abc import ABC, abstractmethod
5
4
  from typing import Dict, List, Type, Union
6
5
 
@@ -10,8 +9,6 @@ from typing_extensions import override
10
9
  # LOCAL
11
10
  from sonatoki.Filters import Filter
12
11
 
13
- LOG = logging.getLogger(__name__)
14
-
15
12
  Number = Union[int, float]
16
13
  Weights = Dict[str, Number]
17
14
 
@@ -37,12 +34,7 @@ class PassFail(Scorer):
37
34
  def score_token(cls, token: str, filters: List[Type[Filter]]) -> Number:
38
35
  for f in filters:
39
36
  if f.filter(token):
40
- score = 1
41
- LOG.debug(
42
- "%12s.%s('%s') = %.2f", cls.__name__, f.__name__, token, score
43
- )
44
- return score
45
- LOG.debug("%12s('%s') = 0.00", cls.__name__, token)
37
+ return 1
46
38
  return 0
47
39
 
48
40
  @classmethod
@@ -86,12 +78,7 @@ class Scaling(Scorer):
86
78
  def score_token(cls, token: str, filters: List[Type[Filter]], scale: int):
87
79
  for i, f in enumerate(filters):
88
80
  if f.filter(token):
89
- score = scale - i
90
- LOG.debug(
91
- "%12s.%s('%s') = %.2f", cls.__name__, f.__name__, token, score
92
- )
93
- return score
94
- LOG.debug("%12s('%s') = 0.00", cls.__name__, token)
81
+ return scale - i
95
82
  return 0
96
83
 
97
84
  @classmethod
sonatoki/Tokenizers.py CHANGED
@@ -1,21 +1,22 @@
1
1
  # STL
2
+ import re
2
3
  from abc import ABC, abstractmethod
3
- from typing import List
4
+ from typing import Set, List
4
5
 
5
6
  # PDM
6
- import regex as re
7
+ import regex
7
8
  from typing_extensions import override
8
9
 
9
- try:
10
- # PDM
11
- import nltk
12
- from nltk.tokenize import sent_tokenize as __sent_tokenize_nltk
13
- from nltk.tokenize import word_tokenize as __word_tokenize_nltk
14
- except ImportError as e:
15
- nltk = e
10
+ # LOCAL
11
+ from sonatoki.utils import regex_escape
12
+ from sonatoki.constants import (
13
+ POSIX_PUNCT,
14
+ UNICODE_PUNCT,
15
+ SENTENCE_PUNCT,
16
+ ALL_PUNCT_RANGES,
17
+ )
16
18
 
17
-
18
- LANGUAGE = "english" # for NLTK
19
+ regex.DEFAULT_VERSION = regex.VERSION1
19
20
 
20
21
 
21
22
  class Tokenizer(ABC):
@@ -24,53 +25,115 @@ class Tokenizer(ABC):
24
25
  def tokenize(cls, s: str) -> List[str]: ...
25
26
 
26
27
 
27
- class NoOpTokenizer(Tokenizer):
28
- """This is a special case that you do not want or need."""
28
+ class SetTokenizer(Tokenizer):
29
+ delimiters: Set[str]
30
+
31
+
32
+ class RegexTokenizer(Tokenizer):
33
+ pattern: "re.Pattern[str]"
29
34
 
30
35
  @classmethod
31
36
  @override
32
37
  def tokenize(cls, s: str) -> List[str]:
33
- return [s]
38
+ return [clean for word in re.split(cls.pattern, s) if (clean := word.strip())]
34
39
 
35
40
 
36
- class RegexTokenizer(Tokenizer):
37
- pattern: "re.Pattern[str]"
41
+ class Regex1Tokenizer(Tokenizer):
42
+ pattern: "regex.Pattern[str]"
38
43
 
39
44
  @classmethod
40
45
  @override
41
46
  def tokenize(cls, s: str) -> List[str]:
42
- return [clean for word in re.split(cls.pattern, s) if (clean := word.strip())]
47
+ return [
48
+ clean for word in regex.split(cls.pattern, s) if (clean := word.strip())
49
+ ]
43
50
 
44
51
 
45
- class WordTokenizerTok(RegexTokenizer):
46
- pattern = re.compile(r"""([\p{Punctuation}\p{posix_punct}]+|\s+)""")
47
- # TODO: are <> or {} that common as *sentence* delims? [] are already a stretch
48
- # TODO: do the typography characters matter?
49
- # NOTE: | / and , are *not* sentence delimiters for my purpose
52
+ class WordTokenizer(SetTokenizer):
53
+ delimiters = set(POSIX_PUNCT + UNICODE_PUNCT)
54
+
55
+ @classmethod
56
+ @override
57
+ def tokenize(cls, s: str) -> List[str]:
58
+ if not s:
59
+ return []
60
+
61
+ tokens: List[str] = []
62
+
63
+ last_match = 0
64
+ last_membership = s[0] in cls.delimiters
65
+ for i, char in enumerate(s):
66
+ mem = char in cls.delimiters
67
+ if mem == last_membership:
68
+ continue
50
69
 
70
+ match = s[last_match:i].split()
71
+ # TODO: kinda sucks? what about unicode whitespace?
72
+ last_match = i
73
+ last_membership = mem
74
+ [tokens.append(t) for t in match if t]
51
75
 
52
- class SentTokenizerTok(RegexTokenizer):
53
- pattern = re.compile(r"""(?<=[.?!:;·…“”"'()\[\]\-]|$)""")
76
+ match = s[last_match:].strip().split()
77
+ if match:
78
+ tokens.extend(match)
79
+
80
+ return tokens
54
81
 
55
82
 
56
83
  class WordTokenizerRe(RegexTokenizer):
57
- pattern = re.compile(r"""(?<=[.?!;:'"-])""")
84
+ pattern = re.compile(rf"""([{ALL_PUNCT_RANGES}]+|\s+)""")
85
+
86
+
87
+ class WordTokenizerRe1(Regex1Tokenizer):
88
+ """Reference implementation for WorkTokenizer."""
89
+
90
+ pattern = regex.compile(r"""([\p{posix_punct}\p{Punctuation}]+|\s+)""")
91
+
92
+
93
+ class SentTokenizer(SetTokenizer):
94
+ delimiters = set(SENTENCE_PUNCT + "\n") # regex does \n with a flag
95
+
96
+ @classmethod
97
+ @override
98
+ def tokenize(cls, s: str) -> List[str]:
99
+ if not s:
100
+ return []
101
+
102
+ tokens: List[str] = []
103
+ last_match = 0
104
+ for i, char in enumerate(s):
105
+ if char not in cls.delimiters:
106
+ continue
107
+
108
+ match = s[last_match : i + 1].strip()
109
+ last_match = i + 1 # newlines can strip but idc
110
+ if not match:
111
+ continue
112
+ tokens.append(match)
113
+
114
+ match = s[last_match:].strip()
115
+ if match:
116
+ tokens.append(match)
117
+
118
+ return tokens
58
119
 
59
120
 
60
121
  class SentTokenizerRe(RegexTokenizer):
61
- pattern = re.compile(r"""(.*?[.?!;:])|(.+?$)""")
122
+ pattern = re.compile(
123
+ rf"""(?<=[{regex_escape(SENTENCE_PUNCT)}])|$""", flags=re.MULTILINE
124
+ )
125
+ # TODO: are <> or {} that common as *sentence* delims? [] are already a stretch
126
+ # TODO: do the typography characters matter?
127
+ # NOTE: | / and , are *not* sentence delimiters for my purpose
62
128
 
63
129
 
64
- if not isinstance(nltk, ImportError):
130
+ class SentTokenizerRe1(Regex1Tokenizer):
131
+ pattern = regex.compile(
132
+ rf"""(?<=[{regex_escape(SENTENCE_PUNCT)}]|$)""", flags=regex.MULTILINE
133
+ )
65
134
 
66
- class WordTokenizerNLTK(Tokenizer):
67
- @classmethod
68
- @override
69
- def tokenize(cls, s: str) -> List[str]:
70
- return __word_tokenize_nltk(text=s, language=LANGUAGE)
71
135
 
72
- class SentTokenizerNLTK(Tokenizer):
73
- @classmethod
74
- @override
75
- def tokenize(cls, s: str) -> List[str]:
76
- return __sent_tokenize_nltk(text=s, language=LANGUAGE)
136
+ __all__ = [
137
+ "WordTokenizer",
138
+ "SentTokenizer",
139
+ ]
sonatoki/constants.py CHANGED
@@ -3,13 +3,28 @@ import json
3
3
  from typing import Dict, List
4
4
  from pathlib import Path
5
5
 
6
+ # LOCAL
7
+ from sonatoki.utils import find_unicode_ranges
8
+
9
+ # `\p{Punctuation}` character class
10
+ UNICODE_PUNCT = r"""!"#%&'()*+,-./:;<=>?@[\]^_`{|}~¡¦§¨©«¬®¯°±´¶·¸»¿×÷˂˃˄˅˒˓˔˕˖˗˘˙˚˛˜˝˞˟˥˦˧˨˩˪˫˭˯˰˱˲˳˴˵˶˷˸˹˺˻˼˽˾˿͵;΄΅·϶҂՚՛՜՝՞՟։֊֍֎־׀׃׆׳״؆؇؈؉؊،؍؎؏؛؝؞؟٪٫٬٭۔۞۩۽۾܀܁܂܃܄܅܆܇܈܉܊܋܌܍߶߷߸߹࠰࠱࠲࠳࠴࠵࠶࠷࠸࠹࠺࠻࠼࠽࠾࡞࢈।॥॰৺৽੶૰୰௳௴௵௶௷௸௺౷౿಄൏൹෴๏๚๛༁༂༃༄༅༆༇༈༉༊་༌།༎༏༐༑༒༓༔༕༖༗༚༛༜༝༞༟༴༶༸༺༻༼༽྅྾྿࿀࿁࿂࿃࿄࿅࿇࿈࿉࿊࿋࿌࿎࿏࿐࿑࿒࿓࿔࿕࿖࿗࿘࿙࿚၊။၌၍၎၏႞႟჻፠፡።፣፤፥፦፧፨᎐᎑᎒᎓᎔᎕᎖᎗᎘᎙᐀᙭᙮᚛᚜᛫᛬᛭᜵᜶។៕៖៘៙៚᠀᠁᠂᠃᠄᠅᠆᠇᠈᠉᠊᥀᥄᥅᧞᧟᧠᧡᧢᧣᧤᧥᧦᧧᧨᧩᧪᧫᧬᧭᧮᧯᧰᧱᧲᧳᧴᧵᧶᧷᧸᧹᧺᧻᧼᧽᧾᧿᨞᨟᪠᪡᪢᪣᪤᪥᪦᪨᪩᪪᪫᪬᪭᭚᭛᭜᭝᭞᭟᭠᭡᭢᭣᭤᭥᭦᭧᭨᭩᭪᭴᭵᭶᭷᭸᭹᭺᭻᭼᭽᭾᯼᯽᯾᯿᰻᰼᰽᰾᰿᱾᱿᳀᳁᳂᳃᳄᳅᳆᳇᳓᾽᾿῀῁῍῎῏῝῞῟῭΅`´῾‐‑‒–—―‖‗‘’‚‛“”„‟†‡•‣․‥…‧‰‱′″‴‵‶‷‸‹›※‼‽‾‿⁀⁁⁂⁃⁄⁅⁆⁇⁈⁉⁊⁋⁌⁍⁎⁏⁐⁑⁒⁓⁔⁕⁖⁗⁘⁙⁚⁛⁜⁝⁞⁺⁻⁼⁽⁾₊₋₌₍₎℀℁℃℄℅℆℈℉℔№℗℘℞℟℠℡™℣℥℧℩℮℺℻⅀⅁⅂⅃⅄⅊⅋⅌⅍⅏↊↋←↑→↓↔↕↖↗↘↙↚↛↜↝↞↟↠↡↢↣↤↥↦↧↨↩↪↫↬↭↮↯↰↱↲↳↴↵↶↷↸↹↺↻↼↽↾↿⇀⇁⇂⇃⇄⇅⇆⇇⇈⇉⇊⇋⇌⇍⇎⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇚⇛⇜⇝⇞⇟⇠⇡⇢⇣⇤⇥⇦⇧⇨⇩⇪⇫⇬⇭⇮⇯⇰⇱⇲⇳⇴⇵⇶⇷⇸⇹⇺⇻⇼⇽⇾⇿∀∁∂∃∄∅∆∇∈∉∊∋∌∍∎∏∐∑−∓∔∕∖∗∘∙√∛∜∝∞∟∠∡∢∣∤∥∦∧∨∩∪∫∬∭∮∯∰∱∲∳∴∵∶∷∸∹∺∻∼∽∾∿≀≁≂≃≄≅≆≇≈≉≊≋≌≍≎≏≐≑≒≓≔≕≖≗≘≙≚≛≜≝≞≟≠≡≢≣≤≥≦≧≨≩≪≫≬≭≮≯≰≱≲≳≴≵≶≷≸≹≺≻≼≽≾≿⊀⊁⊂⊃⊄⊅⊆⊇⊈⊉⊊⊋⊌⊍⊎⊏⊐⊑⊒⊓⊔⊕⊖⊗⊘⊙⊚⊛⊜⊝⊞⊟⊠⊡⊢⊣⊤⊥⊦⊧⊨⊩⊪⊫⊬⊭⊮⊯⊰⊱⊲⊳⊴⊵⊶⊷⊸⊹⊺⊻⊼⊽⊾⊿⋀⋁⋂⋃⋄⋅⋆⋇⋈⋉⋊⋋⋌⋍⋎⋏⋐⋑⋒⋓⋔⋕⋖⋗⋘⋙⋚⋛⋜⋝⋞⋟⋠⋡⋢⋣⋤⋥⋦⋧⋨⋩⋪⋫⋬⋭⋮⋯⋰⋱⋲⋳⋴⋵⋶⋷⋸⋹⋺⋻⋼⋽⋾⋿⌀⌁⌂⌃⌄⌅⌆⌇⌈⌉⌊⌋⌌⌍⌎⌏⌐⌑⌒⌓⌔⌕⌖⌗⌘⌙⌚⌛⌜⌝⌞⌟⌠⌡⌢⌣⌤⌥⌦⌧⌨〈〉⌫⌬⌭⌮⌯⌰⌱⌲⌳⌴⌵⌶⌷⌸⌹⌺⌻⌼⌽⌾⌿⍀⍁⍂⍃⍄⍅⍆⍇⍈⍉⍊⍋⍌⍍⍎⍏⍐⍑⍒⍓⍔⍕⍖⍗⍘⍙⍚⍛⍜⍝⍞⍟⍠⍡⍢⍣⍤⍥⍦⍧⍨⍩⍪⍫⍬⍭⍮⍯⍰⍱⍲⍳⍴⍵⍶⍷⍸⍹⍺⍻⍼⍽⍾⍿⎀⎁⎂⎃⎄⎅⎆⎇⎈⎉⎊⎋⎌⎍⎎⎏⎐⎑⎒⎓⎔⎕⎖⎗⎘⎙⎚⎛⎜⎝⎞⎟⎠⎡⎢⎣⎤⎥⎦⎧⎨⎩⎪⎫⎬⎭⎮⎯⎰⎱⎲⎳⎴⎵⎶⎷⎸⎹⎺⎻⎼⎽⎾⎿⏀⏁⏂⏃⏄⏅⏆⏇⏈⏉⏊⏋⏌⏍⏎⏏⏐⏑⏒⏓⏔⏕⏖⏗⏘⏙⏚⏛⏜⏝⏞⏟⏠⏡⏢⏣⏤⏥⏦⏧⏨⏩⏪⏫⏬⏭⏮⏯⏰⏱⏲⏳⏴⏵⏶⏷⏸⏹⏺⏻⏼⏽⏾⏿␀␁␂␃␄␅␆␇␈␉␊␋␌␍␎␏␐␑␒␓␔␕␖␗␘␙␚␛␜␝␞␟␠␡␢␣␤␥␦⑀⑁⑂⑃⑄⑅⑆⑇⑈⑉⑊⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵─━│┃┄┅┆┇┈┉┊┋┌┍┎┏┐┑┒┓└┕┖┗┘┙┚┛├┝┞┟┠┡┢┣┤┥┦┧┨┩┪┫┬┭┮┯┰┱┲┳┴┵┶┷┸┹┺┻┼┽┾┿╀╁╂╃╄╅╆╇╈╉╊╋╌╍╎╏═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟╠╡╢╣╤╥╦╧╨╩╪╫╬╭╮╯╰╱╲╳╴╵╶╷╸╹╺╻╼╽╾╿▀▁▂▃▄▅▆▇█▉▊▋▌▍▎▏▐░▒▓▔▕▖▗▘▙▚▛▜▝▞▟■□▢▣▤▥▦▧▨▩▪▫▬▭▮▯▰▱▲△▴▵▶▷▸▹►▻▼▽▾▿◀◁◂◃◄◅◆◇◈◉◊○◌◍◎●◐◑◒◓◔◕◖◗◘◙◚◛◜◝◞◟◠◡◢◣◤◥◦◧◨◩◪◫◬◭◮◯◰◱◲◳◴◵◶◷◸◹◺◻◼◽◾◿☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓☔☕☖☗☘☙☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯☰☱☲☳☴☵☶☷☸☹☺☻☼☽☾☿♀♁♂♃♄♅♆♇♈♉♊♋♌♍♎♏♐♑♒♓♔♕♖♗♘♙♚♛♜♝♞♟♠♡♢♣♤♥♦♧♨♩♪♫♬♭♮♯♰♱♲♳♴♵♶♷♸♹♺♻♼♽♾♿⚀⚁⚂⚃⚄⚅⚆⚇⚈⚉⚊⚋⚌⚍⚎⚏⚐⚑⚒⚓⚔⚕⚖⚗⚘⚙⚚⚛⚜⚝⚞⚟⚠⚡⚢⚣⚤⚥⚦⚧⚨⚩⚪⚫⚬⚭⚮⚯⚰⚱⚲⚳⚴⚵⚶⚷⚸⚹⚺⚻⚼⚽⚾⚿⛀⛁⛂⛃⛄⛅⛆⛇⛈⛉⛊⛋⛌⛍⛎⛏⛐⛑⛒⛓⛔⛕⛖⛗⛘⛙⛚⛛⛜⛝⛞⛟⛠⛡⛢⛣⛤⛥⛦⛧⛨⛩⛪⛫⛬⛭⛮⛯⛰⛱⛲⛳⛴⛵⛶⛷⛸⛹⛺⛻⛼⛽⛾⛿✀✁✂✃✄✅✆✇✈✉✊✋✌✍✎✏✐✑✒✓✔✕✖✗✘✙✚✛✜✝✞✟✠✡✢✣✤✥✦✧✨✩✪✫✬✭✮✯✰✱✲✳✴✵✶✷✸✹✺✻✼✽✾✿❀❁❂❃❄❅❆❇❈❉❊❋❌❍❎❏❐❑❒❓❔❕❖❗❘❙❚❛❜❝❞❟❠❡❢❣❤❥❦❧❨❩❪❫❬❭❮❯❰❱❲❳❴❵➔➕➖➗➘➙➚➛➜➝➞➟➠➡➢➣➤➥➦➧➨➩➪➫➬➭➮➯➰➱➲➳➴➵➶➷➸➹➺➻➼➽➾➿⟀⟁⟂⟃⟄⟅⟆⟇⟈⟉⟊⟋⟌⟍⟎⟏⟐⟑⟒⟓⟔⟕⟖⟗⟘⟙⟚⟛⟜⟝⟞⟟⟠⟡⟢⟣⟤⟥⟦⟧⟨⟩⟪⟫⟬⟭⟮⟯⟰⟱⟲⟳⟴⟵⟶⟷⟸⟹⟺⟻⟼⟽⟾⟿⠀⠁⠂⠃⠄⠅⠆⠇⠈⠉⠊⠋⠌⠍⠎⠏⠐⠑⠒⠓⠔⠕⠖⠗⠘⠙⠚⠛⠜⠝⠞⠟⠠⠡⠢⠣⠤⠥⠦⠧⠨⠩⠪⠫⠬⠭⠮⠯⠰⠱⠲⠳⠴⠵⠶⠷⠸⠹⠺⠻⠼⠽⠾⠿⡀⡁⡂⡃⡄⡅⡆⡇⡈⡉⡊⡋⡌⡍⡎⡏⡐⡑⡒⡓⡔⡕⡖⡗⡘⡙⡚⡛⡜⡝⡞⡟⡠⡡⡢⡣⡤⡥⡦⡧⡨⡩⡪⡫⡬⡭⡮⡯⡰⡱⡲⡳⡴⡵⡶⡷⡸⡹⡺⡻⡼⡽⡾⡿⢀⢁⢂⢃⢄⢅⢆⢇⢈⢉⢊⢋⢌⢍⢎⢏⢐⢑⢒⢓⢔⢕⢖⢗⢘⢙⢚⢛⢜⢝⢞⢟⢠⢡⢢⢣⢤⢥⢦⢧⢨⢩⢪⢫⢬⢭⢮⢯⢰⢱⢲⢳⢴⢵⢶⢷⢸⢹⢺⢻⢼⢽⢾⢿⣀⣁⣂⣃⣄⣅⣆⣇⣈⣉⣊⣋⣌⣍⣎⣏⣐⣑⣒⣓⣔⣕⣖⣗⣘⣙⣚⣛⣜⣝⣞⣟⣠⣡⣢⣣⣤⣥⣦⣧⣨⣩⣪⣫⣬⣭⣮⣯⣰⣱⣲⣳⣴⣵⣶⣷⣸⣹⣺⣻⣼⣽⣾⣿⤀⤁⤂⤃⤄⤅⤆⤇⤈⤉⤊⤋⤌⤍⤎⤏⤐⤑⤒⤓⤔⤕⤖⤗⤘⤙⤚⤛⤜⤝⤞⤟⤠⤡⤢⤣⤤⤥⤦⤧⤨⤩⤪⤫⤬⤭⤮⤯⤰⤱⤲⤳⤴⤵⤶⤷⤸⤹⤺⤻⤼⤽⤾⤿⥀⥁⥂⥃⥄⥅⥆⥇⥈⥉⥊⥋⥌⥍⥎⥏⥐⥑⥒⥓⥔⥕⥖⥗⥘⥙⥚⥛⥜⥝⥞⥟⥠⥡⥢⥣⥤⥥⥦⥧⥨⥩⥪⥫⥬⥭⥮⥯⥰⥱⥲⥳⥴⥵⥶⥷⥸⥹⥺⥻⥼⥽⥾⥿⦀⦁⦂⦃⦄⦅⦆⦇⦈⦉⦊⦋⦌⦍⦎⦏⦐⦑⦒⦓⦔⦕⦖⦗⦘⦙⦚⦛⦜⦝⦞⦟⦠⦡⦢⦣⦤⦥⦦⦧⦨⦩⦪⦫⦬⦭⦮⦯⦰⦱⦲⦳⦴⦵⦶⦷⦸⦹⦺⦻⦼⦽⦾⦿⧀⧁⧂⧃⧄⧅⧆⧇⧈⧉⧊⧋⧌⧍⧎⧏⧐⧑⧒⧓⧔⧕⧖⧗⧘⧙⧚⧛⧜⧝⧞⧟⧠⧡⧢⧣⧤⧥⧦⧧⧨⧩⧪⧫⧬⧭⧮⧯⧰⧱⧲⧳⧴⧵⧶⧷⧸⧹⧺⧻⧼⧽⧾⧿⨀⨁⨂⨃⨄⨅⨆⨇⨈⨉⨊⨋⨌⨍⨎⨏⨐⨑⨒⨓⨔⨕⨖⨗⨘⨙⨚⨛⨜⨝⨞⨟⨠⨡⨢⨣⨤⨥⨦⨧⨨⨩⨪⨫⨬⨭⨮⨯⨰⨱⨲⨳⨴⨵⨶⨷⨸⨹⨺⨻⨼⨽⨾⨿⩀⩁⩂⩃⩄⩅⩆⩇⩈⩉⩊⩋⩌⩍⩎⩏⩐⩑⩒⩓⩔⩕⩖⩗⩘⩙⩚⩛⩜⩝⩞⩟⩠⩡⩢⩣⩤⩥⩦⩧⩨⩩⩪⩫⩬⩭⩮⩯⩰⩱⩲⩳⩴⩵⩶⩷⩸⩹⩺⩻⩼⩽⩾⩿⪀⪁⪂⪃⪄⪅⪆⪇⪈⪉⪊⪋⪌⪍⪎⪏⪐⪑⪒⪓⪔⪕⪖⪗⪘⪙⪚⪛⪜⪝⪞⪟⪠⪡⪢⪣⪤⪥⪦⪧⪨⪩⪪⪫⪬⪭⪮⪯⪰⪱⪲⪳⪴⪵⪶⪷⪸⪹⪺⪻⪼⪽⪾⪿⫀⫁⫂⫃⫄⫅⫆⫇⫈⫉⫊⫋⫌⫍⫎⫏⫐⫑⫒⫓⫔⫕⫖⫗⫘⫙⫚⫛⫝̸⫝⫞⫟⫠⫡⫢⫣⫤⫥⫦⫧⫨⫩⫪⫫⫬⫭⫮⫯⫰⫱⫲⫳⫴⫵⫶⫷⫸⫹⫺⫻⫼⫽⫾⫿⬀⬁⬂⬃⬄⬅⬆⬇⬈⬉⬊⬋⬌⬍⬎⬏⬐⬑⬒⬓⬔⬕⬖⬗⬘⬙⬚⬛⬜⬝⬞⬟⬠⬡⬢⬣⬤⬥⬦⬧⬨⬩⬪⬫⬬⬭⬮⬯⬰⬱⬲⬳⬴⬵⬶⬷⬸⬹⬺⬻⬼⬽⬾⬿⭀⭁⭂⭃⭄⭅⭆⭇⭈⭉⭊⭋⭌⭍⭎⭏⭐⭑⭒⭓⭔⭕⭖⭗⭘⭙⭚⭛⭜⭝⭞⭟⭠⭡⭢⭣⭤⭥⭦⭧⭨⭩⭪⭫⭬⭭⭮⭯⭰⭱⭲⭳⭶⭷⭸⭹⭺⭻⭼⭽⭾⭿⮀⮁⮂⮃⮄⮅⮆⮇⮈⮉⮊⮋⮌⮍⮎⮏⮐⮑⮒⮓⮔⮕⮗⮘⮙⮚⮛⮜⮝⮞⮟⮠⮡⮢⮣⮤⮥⮦⮧⮨⮩⮪⮫⮬⮭⮮⮯⮰⮱⮲⮳⮴⮵⮶⮷⮸⮹⮺⮻⮼⮽⮾⮿⯀⯁⯂⯃⯄⯅⯆⯇⯈⯉⯊⯋⯌⯍⯎⯏⯐⯑⯒⯓⯔⯕⯖⯗⯘⯙⯚⯛⯜⯝⯞⯟⯠⯡⯢⯣⯤⯥⯦⯧⯨⯩⯪⯫⯬⯭⯮⯯⯰⯱⯲⯳⯴⯵⯶⯷⯸⯹⯺⯻⯼⯽⯾⯿⳥⳦⳧⳨⳩⳪⳹⳺⳻⳼⳾⳿⵰⸀⸁⸂⸃⸄⸅⸆⸇⸈⸉⸊⸋⸌⸍⸎⸏⸐⸑⸒⸓⸔⸕⸖⸗⸘⸙⸚⸛⸜⸝⸞⸟⸠⸡⸢⸣⸤⸥⸦⸧⸨⸩⸪⸫⸬⸭⸮⸰⸱⸲⸳⸴⸵⸶⸷⸸⸹⸺⸻⸼⸽⸾⸿⹀⹁⹂⹃⹄⹅⹆⹇⹈⹉⹊⹋⹌⹍⹎⹏⹐⹑⹒⹓⹔⹕⹖⹗⹘⹙⹚⹛⹜⹝⺀⺁⺂⺃⺄⺅⺆⺇⺈⺉⺊⺋⺌⺍⺎⺏⺐⺑⺒⺓⺔⺕⺖⺗⺘⺙⺛⺜⺝⺞⺟⺠⺡⺢⺣⺤⺥⺦⺧⺨⺩⺪⺫⺬⺭⺮⺯⺰⺱⺲⺳⺴⺵⺶⺷⺸⺹⺺⺻⺼⺽⺾⺿⻀⻁⻂⻃⻄⻅⻆⻇⻈⻉⻊⻋⻌⻍⻎⻏⻐⻑⻒⻓⻔⻕⻖⻗⻘⻙⻚⻛⻜⻝⻞⻟⻠⻡⻢⻣⻤⻥⻦⻧⻨⻩⻪⻫⻬⻭⻮⻯⻰⻱⻲⻳⼀⼁⼂⼃⼄⼅⼆⼇⼈⼉⼊⼋⼌⼍⼎⼏⼐⼑⼒⼓⼔⼕⼖⼗⼘⼙⼚⼛⼜⼝⼞⼟⼠⼡⼢⼣⼤⼥⼦⼧⼨⼩⼪⼫⼬⼭⼮⼯⼰⼱⼲⼳⼴⼵⼶⼷⼸⼹⼺⼻⼼⼽⼾⼿⽀⽁⽂⽃⽄⽅⽆⽇⽈⽉⽊⽋⽌⽍⽎⽏⽐⽑⽒⽓⽔⽕⽖⽗⽘⽙⽚⽛⽜⽝⽞⽟⽠⽡⽢⽣⽤⽥⽦⽧⽨⽩⽪⽫⽬⽭⽮⽯⽰⽱⽲⽳⽴⽵⽶⽷⽸⽹⽺⽻⽼⽽⽾⽿⾀⾁⾂⾃⾄⾅⾆⾇⾈⾉⾊⾋⾌⾍⾎⾏⾐⾑⾒⾓⾔⾕⾖⾗⾘⾙⾚⾛⾜⾝⾞⾟⾠⾡⾢⾣⾤⾥⾦⾧⾨⾩⾪⾫⾬⾭⾮⾯⾰⾱⾲⾳⾴⾵⾶⾷⾸⾹⾺⾻⾼⾽⾾⾿⿀⿁⿂⿃⿄⿅⿆⿇⿈⿉⿊⿋⿌⿍⿎⿏⿐⿑⿒⿓⿔⿕⿰⿱⿲⿳⿴⿵⿶⿷⿸⿹⿺⿻⿼⿽⿾⿿、。〃〄〈〉《》「」『』【】〒〓〔〕〖〗〘〙〚〛〜〝〞〟〠〰〶〷〽〾〿゛゜゠・㆐㆑㆖㆗㆘㆙㆚㆛㆜㆝㆞㆟㇀㇁㇂㇃㇄㇅㇆㇇㇈㇉㇊㇋㇌㇍㇎㇏㇐㇑㇒㇓㇔㇕㇖㇗㇘㇙㇚㇛㇜㇝㇞㇟㇠㇡㇢㇣㇯㈀㈁㈂㈃㈄㈅㈆㈇㈈㈉㈊㈋㈌㈍㈎㈏㈐㈑㈒㈓㈔㈕㈖㈗㈘㈙㈚㈛㈜㈝㈞㈪㈫㈬㈭㈮㈯㈰㈱㈲㈳㈴㈵㈶㈷㈸㈹㈺㈻㈼㈽㈾㈿㉀㉁㉂㉃㉄㉅㉆㉇㉐㉠㉡㉢㉣㉤㉥㉦㉧㉨㉩㉪㉫㉬㉭㉮㉯㉰㉱㉲㉳㉴㉵㉶㉷㉸㉹㉺㉻㉼㉽㉾㉿㊊㊋㊌㊍㊎㊏㊐㊑㊒㊓㊔㊕㊖㊗㊘㊙㊚㊛㊜㊝㊞㊟㊠㊡㊢㊣㊤㊥㊦㊧㊨㊩㊪㊫㊬㊭㊮㊯㊰㋀㋁㋂㋃㋄㋅㋆㋇㋈㋉㋊㋋㋌㋍㋎㋏㋐㋑㋒㋓㋔㋕㋖㋗㋘㋙㋚㋛㋜㋝㋞㋟㋠㋡㋢㋣㋤㋥㋦㋧㋨㋩㋪㋫㋬㋭㋮㋯㋰㋱㋲㋳㋴㋵㋶㋷㋸㋹㋺㋻㋼㋽㋾㋿㌀㌁㌂㌃㌄㌅㌆㌇㌈㌉㌊㌋㌌㌍㌎㌏㌐㌑㌒㌓㌔㌕㌖㌗㌘㌙㌚㌛㌜㌝㌞㌟㌠㌡㌢㌣㌤㌥㌦㌧㌨㌩㌪㌫㌬㌭㌮㌯㌰㌱㌲㌳㌴㌵㌶㌷㌸㌹㌺㌻㌼㌽㌾㌿㍀㍁㍂㍃㍄㍅㍆㍇㍈㍉㍊㍋㍌㍍㍎㍏㍐㍑㍒㍓㍔㍕㍖㍗㍘㍙㍚㍛㍜㍝㍞㍟㍠㍡㍢㍣㍤㍥㍦㍧㍨㍩㍪㍫㍬㍭㍮㍯㍰㍱㍲㍳㍴㍵㍶㍷㍸㍹㍺㍻㍼㍽㍾㍿㎀㎁㎂㎃㎄㎅㎆㎇㎈㎉㎊㎋㎌㎍㎎㎏㎐㎑㎒㎓㎔㎕㎖㎗㎘㎙㎚㎛㎜㎝㎞㎟㎠㎡㎢㎣㎤㎥㎦㎧㎨㎩㎪㎫㎬㎭㎮㎯㎰㎱㎲㎳㎴㎵㎶㎷㎸㎹㎺㎻㎼㎽㎾㎿㏀㏁㏂㏃㏄㏅㏆㏇㏈㏉㏊㏋㏌㏍㏎㏏㏐㏑㏒㏓㏔㏕㏖㏗㏘㏙㏚㏛㏜㏝㏞㏟㏠㏡㏢㏣㏤㏥㏦㏧㏨㏩㏪㏫㏬㏭㏮㏯㏰㏱㏲㏳㏴㏵㏶㏷㏸㏹㏺㏻㏼㏽㏾㏿䷀䷁䷂䷃䷄䷅䷆䷇䷈䷉䷊䷋䷌䷍䷎䷏䷐䷑䷒䷓䷔䷕䷖䷗䷘䷙䷚䷛䷜䷝䷞䷟䷠䷡䷢䷣䷤䷥䷦䷧䷨䷩䷪䷫䷬䷭䷮䷯䷰䷱䷲䷳䷴䷵䷶䷷䷸䷹䷺䷻䷼䷽䷾䷿꒐꒑꒒꒓꒔꒕꒖꒗꒘꒙꒚꒛꒜꒝꒞꒟꒠꒡꒢꒣꒤꒥꒦꒧꒨꒩꒪꒫꒬꒭꒮꒯꒰꒱꒲꒳꒴꒵꒶꒷꒸꒹꒺꒻꒼꒽꒾꒿꓀꓁꓂꓃꓄꓅꓆꓾꓿꘍꘎꘏꙳꙾꛲꛳꛴꛵꛶꛷꜀꜁꜂꜃꜄꜅꜆꜇꜈꜉꜊꜋꜌꜍꜎꜏꜐꜑꜒꜓꜔꜕꜖꜠꜡꞉꞊꠨꠩꠪꠫꠶꠷꠹꡴꡵꡶꡷꣎꣏꣸꣹꣺꣼꤮꤯꥟꧁꧂꧃꧄꧅꧆꧇꧈꧉꧊꧋꧌꧍꧞꧟꩜꩝꩞꩟꩷꩸꩹꫞꫟꫰꫱꭛꭪꭫꯫﬩﮲﮳﮴﮵﮶﮷﮸﮹﮺﮻﮼﮽﮾﮿﯀﯁﯂﴾﴿﵀﵁﵂﵃﵄﵅﵆﵇﵈﵉﵊﵋﵌﵍﵎﵏﷏﷽﷾﷿︐︑︒︓︔︕︖︗︘︙︰︱︲︳︴︵︶︷︸︹︺︻︼︽︾︿﹀﹁﹂﹃﹄﹅﹆﹇﹈﹉﹊﹋﹌﹍﹎﹏﹐﹑﹒﹔﹕﹖﹗﹘﹙﹚﹛﹜﹝﹞﹟﹠﹡﹢﹣﹤﹥﹦﹨﹪﹫!"#%&'()*+,-./:;<=>?@[\]^_`{|}~⦅⦆。「」、・¬ ̄¦│←↑→↓■○�𐄀𐄁𐄂𐄷𐄸𐄹𐄺𐄻𐄼𐄽𐄾𐄿𐅹𐅺𐅻𐅼𐅽𐅾𐅿𐆀𐆁𐆂𐆃𐆄𐆅𐆆𐆇𐆈𐆉𐆌𐆍𐆎𐆐𐆑𐆒𐆓𐆔𐆕𐆖𐆗𐆘𐆙𐆚𐆛𐆜𐆠𐇐𐇑𐇒𐇓𐇔𐇕𐇖𐇗𐇘𐇙𐇚𐇛𐇜𐇝𐇞𐇟𐇠𐇡𐇢𐇣𐇤𐇥𐇦𐇧𐇨𐇩𐇪𐇫𐇬𐇭𐇮𐇯𐇰𐇱𐇲𐇳𐇴𐇵𐇶𐇷𐇸𐇹𐇺𐇻𐇼𐎟𐏐𐕯𐡗𐡷𐡸𐤟𐤿𐩐𐩑𐩒𐩓𐩔𐩕𐩖𐩗𐩘𐩿𐫈𐫰𐫱𐫲𐫳𐫴𐫵𐫶𐬹𐬺𐬻𐬼𐬽𐬾𐬿𐮙𐮚𐮛𐮜𐺭𐽕𐽖𐽗𐽘𐽙𐾆𐾇𐾈𐾉𑁇𑁈𑁉𑁊𑁋𑁌𑁍𑂻𑂼𑂾𑂿𑃀𑃁𑅀𑅁𑅂𑅃𑅴𑅵𑇅𑇆𑇇𑇈𑇍𑇛𑇝𑇞𑇟𑈸𑈹𑈺𑈻𑈼𑈽𑊩𑑋𑑌𑑍𑑎𑑏𑑚𑑛𑑝𑓆𑗁𑗂𑗃𑗄𑗅𑗆𑗇𑗈𑗉𑗊𑗋𑗌𑗍𑗎𑗏𑗐𑗑𑗒𑗓𑗔𑗕𑗖𑗗𑙁𑙂𑙃𑙠𑙡𑙢𑙣𑙤𑙥𑙦𑙧𑙨𑙩𑙪𑙫𑙬𑚹𑜼𑜽𑜾𑜿𑠻𑥄𑥅𑥆𑧢𑨿𑩀𑩁𑩂𑩃𑩄𑩅𑩆𑪚𑪛𑪜𑪞𑪟𑪠𑪡𑪢𑬀𑬁𑬂𑬃𑬄𑬅𑬆𑬇𑬈𑬉𑱁𑱂𑱃𑱄𑱅𑱰𑱱𑻷𑻸𑽃𑽄𑽅𑽆𑽇𑽈𑽉𑽊𑽋𑽌𑽍𑽎𑽏𑿕𑿖𑿗𑿘𑿙𑿚𑿛𑿜𑿡𑿢𑿣𑿤𑿥𑿦𑿧𑿨𑿩𑿪𑿫𑿬𑿭𑿮𑿯𑿰𑿱𑿿𒑰𒑱𒑲𒑳𒑴𒿱𒿲𖩮𖩯𖫵𖬷𖬸𖬹𖬺𖬻𖬼𖬽𖬾𖬿𖭄𖭅𖺗𖺘𖺙𖺚𖿢𛲜𛲟𜽐𜽑𜽒𜽓𜽔𜽕𜽖𜽗𜽘𜽙𜽚𜽛𜽜𜽝𜽞𜽟𜽠𜽡𜽢𜽣𜽤𜽥𜽦𜽧𜽨𜽩𜽪𜽫𜽬𜽭𜽮𜽯𜽰𜽱𜽲𜽳𜽴𜽵𜽶𜽷𜽸𜽹𜽺𜽻𜽼𜽽𜽾𜽿𜾀𜾁𜾂𜾃𜾄𜾅𜾆𜾇𜾈𜾉𜾊𜾋𜾌𜾍𜾎𜾏𜾐𜾑𜾒𜾓𜾔𜾕𜾖𜾗𜾘𜾙𜾚𜾛𜾜𜾝𜾞𜾟𜾠𜾡𜾢𜾣𜾤𜾥𜾦𜾧𜾨𜾩𜾪𜾫𜾬𜾭𜾮𜾯𜾰𜾱𜾲𜾳𜾴𜾵𜾶𜾷𜾸𜾹𜾺𜾻𜾼𜾽𜾾𜾿𜿀𜿁𜿂𜿃𝀀𝀁𝀂𝀃𝀄𝀅𝀆𝀇𝀈𝀉𝀊𝀋𝀌𝀍𝀎𝀏𝀐𝀑𝀒𝀓𝀔𝀕𝀖𝀗𝀘𝀙𝀚𝀛𝀜𝀝𝀞𝀟𝀠𝀡𝀢𝀣𝀤𝀥𝀦𝀧𝀨𝀩𝀪𝀫𝀬𝀭𝀮𝀯𝀰𝀱𝀲𝀳𝀴𝀵𝀶𝀷𝀸𝀹𝀺𝀻𝀼𝀽𝀾𝀿𝁀𝁁𝁂𝁃𝁄𝁅𝁆𝁇𝁈𝁉𝁊𝁋𝁌𝁍𝁎𝁏𝁐𝁑𝁒𝁓𝁔𝁕𝁖𝁗𝁘𝁙𝁚𝁛𝁜𝁝𝁞𝁟𝁠𝁡𝁢𝁣𝁤𝁥𝁦𝁧𝁨𝁩𝁪𝁫𝁬𝁭𝁮𝁯𝁰𝁱𝁲𝁳𝁴𝁵𝁶𝁷𝁸𝁹𝁺𝁻𝁼𝁽𝁾𝁿𝂀𝂁𝂂𝂃𝂄𝂅𝂆𝂇𝂈𝂉𝂊𝂋𝂌𝂍𝂎𝂏𝂐𝂑𝂒𝂓𝂔𝂕𝂖𝂗𝂘𝂙𝂚𝂛𝂜𝂝𝂞𝂟𝂠𝂡𝂢𝂣𝂤𝂥𝂦𝂧𝂨𝂩𝂪𝂫𝂬𝂭𝂮𝂯𝂰𝂱𝂲𝂳𝂴𝂵𝂶𝂷𝂸𝂹𝂺𝂻𝂼𝂽𝂾𝂿𝃀𝃁𝃂𝃃𝃄𝃅𝃆𝃇𝃈𝃉𝃊𝃋𝃌𝃍𝃎𝃏𝃐𝃑𝃒𝃓𝃔𝃕𝃖𝃗𝃘𝃙𝃚𝃛𝃜𝃝𝃞𝃟𝃠𝃡𝃢𝃣𝃤𝃥𝃦𝃧𝃨𝃩𝃪𝃫𝃬𝃭𝃮𝃯𝃰𝃱𝃲𝃳𝃴𝃵𝄀𝄁𝄂𝄃𝄄𝄅𝄆𝄇𝄈𝄉𝄊𝄋𝄌𝄍𝄎𝄏𝄐𝄑𝄒𝄓𝄔𝄕𝄖𝄗𝄘𝄙𝄚𝄛𝄜𝄝𝄞𝄟𝄠𝄡𝄢𝄣𝄤𝄥𝄦𝄩𝄪𝄫𝄬𝄭𝄮𝄯𝄰𝄱𝄲𝄳𝄴𝄵𝄶𝄷𝄸𝄹𝄺𝄻𝄼𝄽𝄾𝄿𝅀𝅁𝅂𝅃𝅄𝅅𝅆𝅇𝅈𝅉𝅊𝅋𝅌𝅍𝅎𝅏𝅐𝅑𝅒𝅓𝅔𝅕𝅖𝅗𝅘𝅙𝅚𝅛𝅜𝅝𝅗𝅥𝅘𝅥𝅘𝅥𝅮𝅘𝅥𝅯𝅘𝅥𝅰𝅘𝅥𝅱𝅘𝅥𝅲𝅪𝅫𝅬𝆃𝆄𝆌𝆍𝆎𝆏𝆐𝆑𝆒𝆓𝆔𝆕𝆖𝆗𝆘𝆙𝆚𝆛𝆜𝆝𝆞𝆟𝆠𝆡𝆢𝆣𝆤𝆥𝆦𝆧𝆨𝆩𝆮𝆯𝆰𝆱𝆲𝆳𝆴𝆵𝆶𝆷𝆸𝆹𝆺𝆹𝅥𝆺𝅥𝆹𝅥𝅮𝆺𝅥𝅮𝆹𝅥𝅯𝆺𝅥𝅯𝇁𝇂𝇃𝇄𝇅𝇆𝇇𝇈𝇉𝇊𝇋𝇌𝇍𝇎𝇏𝇐𝇑𝇒𝇓𝇔𝇕𝇖𝇗𝇘𝇙𝇚𝇛𝇜𝇝𝇞𝇟𝇠𝇡𝇢𝇣𝇤𝇥𝇦𝇧𝇨𝇩𝇪𝈀𝈁𝈂𝈃𝈄𝈅𝈆𝈇𝈈𝈉𝈊𝈋𝈌𝈍𝈎𝈏𝈐𝈑𝈒𝈓𝈔𝈕𝈖𝈗𝈘𝈙𝈚𝈛𝈜𝈝𝈞𝈟𝈠𝈡𝈢𝈣𝈤𝈥𝈦𝈧𝈨𝈩𝈪𝈫𝈬𝈭𝈮𝈯𝈰𝈱𝈲𝈳𝈴𝈵𝈶𝈷𝈸𝈹𝈺𝈻𝈼𝈽𝈾𝈿𝉀𝉁𝉅𝌀𝌁𝌂𝌃𝌄𝌅𝌆𝌇𝌈𝌉𝌊𝌋𝌌𝌍𝌎𝌏𝌐𝌑𝌒𝌓𝌔𝌕𝌖𝌗𝌘𝌙𝌚𝌛𝌜𝌝𝌞𝌟𝌠𝌡𝌢𝌣𝌤𝌥𝌦𝌧𝌨𝌩𝌪𝌫𝌬𝌭𝌮𝌯𝌰𝌱𝌲𝌳𝌴𝌵𝌶𝌷𝌸𝌹𝌺𝌻𝌼𝌽𝌾𝌿𝍀𝍁𝍂𝍃𝍄𝍅𝍆𝍇𝍈𝍉𝍊𝍋𝍌𝍍𝍎𝍏𝍐𝍑𝍒𝍓𝍔𝍕𝍖𝛁𝛛𝛻𝜕𝜵𝝏𝝯𝞉𝞩𝟃𝠀𝠁𝠂𝠃𝠄𝠅𝠆𝠇𝠈𝠉𝠊𝠋𝠌𝠍𝠎𝠏𝠐𝠑𝠒𝠓𝠔𝠕𝠖𝠗𝠘𝠙𝠚𝠛𝠜𝠝𝠞𝠟𝠠𝠡𝠢𝠣𝠤𝠥𝠦𝠧𝠨𝠩𝠪𝠫𝠬𝠭𝠮𝠯𝠰𝠱𝠲𝠳𝠴𝠵𝠶𝠷𝠸𝠹𝠺𝠻𝠼𝠽𝠾𝠿𝡀𝡁𝡂𝡃𝡄𝡅𝡆𝡇𝡈𝡉𝡊𝡋𝡌𝡍𝡎𝡏𝡐𝡑𝡒𝡓𝡔𝡕𝡖𝡗𝡘𝡙𝡚𝡛𝡜𝡝𝡞𝡟𝡠𝡡𝡢𝡣𝡤𝡥𝡦𝡧𝡨𝡩𝡪𝡫𝡬𝡭𝡮𝡯𝡰𝡱𝡲𝡳𝡴𝡵𝡶𝡷𝡸𝡹𝡺𝡻𝡼𝡽𝡾𝡿𝢀𝢁𝢂𝢃𝢄𝢅𝢆𝢇𝢈𝢉𝢊𝢋𝢌𝢍𝢎𝢏𝢐𝢑𝢒𝢓𝢔𝢕𝢖𝢗𝢘𝢙𝢚𝢛𝢜𝢝𝢞𝢟𝢠𝢡𝢢𝢣𝢤𝢥𝢦𝢧𝢨𝢩𝢪𝢫𝢬𝢭𝢮𝢯𝢰𝢱𝢲𝢳𝢴𝢵𝢶𝢷𝢸𝢹𝢺𝢻𝢼𝢽𝢾𝢿𝣀𝣁𝣂𝣃𝣄𝣅𝣆𝣇𝣈𝣉𝣊𝣋𝣌𝣍𝣎𝣏𝣐𝣑𝣒𝣓𝣔𝣕𝣖𝣗𝣘𝣙𝣚𝣛𝣜𝣝𝣞𝣟𝣠𝣡𝣢𝣣𝣤𝣥𝣦𝣧𝣨𝣩𝣪𝣫𝣬𝣭𝣮𝣯𝣰𝣱𝣲𝣳𝣴𝣵𝣶𝣷𝣸𝣹𝣺𝣻𝣼𝣽𝣾𝣿𝤀𝤁𝤂𝤃𝤄𝤅𝤆𝤇𝤈𝤉𝤊𝤋𝤌𝤍𝤎𝤏𝤐𝤑𝤒𝤓𝤔𝤕𝤖𝤗𝤘𝤙𝤚𝤛𝤜𝤝𝤞𝤟𝤠𝤡𝤢𝤣𝤤𝤥𝤦𝤧𝤨𝤩𝤪𝤫𝤬𝤭𝤮𝤯𝤰𝤱𝤲𝤳𝤴𝤵𝤶𝤷𝤸𝤹𝤺𝤻𝤼𝤽𝤾𝤿𝥀𝥁𝥂𝥃𝥄𝥅𝥆𝥇𝥈𝥉𝥊𝥋𝥌𝥍𝥎𝥏𝥐𝥑𝥒𝥓𝥔𝥕𝥖𝥗𝥘𝥙𝥚𝥛𝥜𝥝𝥞𝥟𝥠𝥡𝥢𝥣𝥤𝥥𝥦𝥧𝥨𝥩𝥪𝥫𝥬𝥭𝥮𝥯𝥰𝥱𝥲𝥳𝥴𝥵𝥶𝥷𝥸𝥹𝥺𝥻𝥼𝥽𝥾𝥿𝦀𝦁𝦂𝦃𝦄𝦅𝦆𝦇𝦈𝦉𝦊𝦋𝦌𝦍𝦎𝦏𝦐𝦑𝦒𝦓𝦔𝦕𝦖𝦗𝦘𝦙𝦚𝦛𝦜𝦝𝦞𝦟𝦠𝦡𝦢𝦣𝦤𝦥𝦦𝦧𝦨𝦩𝦪𝦫𝦬𝦭𝦮𝦯𝦰𝦱𝦲𝦳𝦴𝦵𝦶𝦷𝦸𝦹𝦺𝦻𝦼𝦽𝦾𝦿𝧀𝧁𝧂𝧃𝧄𝧅𝧆𝧇𝧈𝧉𝧊𝧋𝧌𝧍𝧎𝧏𝧐𝧑𝧒𝧓𝧔𝧕𝧖𝧗𝧘𝧙𝧚𝧛𝧜𝧝𝧞𝧟𝧠𝧡𝧢𝧣𝧤𝧥𝧦𝧧𝧨𝧩𝧪𝧫𝧬𝧭𝧮𝧯𝧰𝧱𝧲𝧳𝧴𝧵𝧶𝧷𝧸𝧹𝧺𝧻𝧼𝧽𝧾𝧿𝨷𝨸𝨹𝨺𝩭𝩮𝩯𝩰𝩱𝩲𝩳𝩴𝩶𝩷𝩸𝩹𝩺𝩻𝩼𝩽𝩾𝩿𝪀𝪁𝪂𝪃𝪅𝪆𝪇𝪈𝪉𝪊𝪋𞅏𞥞𞥟𞲬𞴮𞻰𞻱🀀🀁🀂🀃🀄🀅🀆🀇🀈🀉🀊🀋🀌🀍🀎🀏🀐🀑🀒🀓🀔🀕🀖🀗🀘🀙🀚🀛🀜🀝🀞🀟🀠🀡🀢🀣🀤🀥🀦🀧🀨🀩🀪🀫🀰🀱🀲🀳🀴🀵🀶🀷🀸🀹🀺🀻🀼🀽🀾🀿🁀🁁🁂🁃🁄🁅🁆🁇🁈🁉🁊🁋🁌🁍🁎🁏🁐🁑🁒🁓🁔🁕🁖🁗🁘🁙🁚🁛🁜🁝🁞🁟🁠🁡🁢🁣🁤🁥🁦🁧🁨🁩🁪🁫🁬🁭🁮🁯🁰🁱🁲🁳🁴🁵🁶🁷🁸🁹🁺🁻🁼🁽🁾🁿🂀🂁🂂🂃🂄🂅🂆🂇🂈🂉🂊🂋🂌🂍🂎🂏🂐🂑🂒🂓🂠🂡🂢🂣🂤🂥🂦🂧🂨🂩🂪🂫🂬🂭🂮🂱🂲🂳🂴🂵🂶🂷🂸🂹🂺🂻🂼🂽🂾🂿🃁🃂🃃🃄🃅🃆🃇🃈🃉🃊🃋🃌🃍🃎🃏🃑🃒🃓🃔🃕🃖🃗🃘🃙🃚🃛🃜🃝🃞🃟🃠🃡🃢🃣🃤🃥🃦🃧🃨🃩🃪🃫🃬🃭🃮🃯🃰🃱🃲🃳🃴🃵🄍🄎🄏🄐🄑🄒🄓🄔🄕🄖🄗🄘🄙🄚🄛🄜🄝🄞🄟🄠🄡🄢🄣🄤🄥🄦🄧🄨🄩🄪🄫🄬🄭🄮🄯🅊🅋🅌🅍🅎🅏🅪🅫🅬🅭🅮🅯🆊🆋🆌🆍🆎🆏🆐🆑🆒🆓🆔🆕🆖🆗🆘🆙🆚🆛🆜🆝🆞🆟🆠🆡🆢🆣🆤🆥🆦🆧🆨🆩🆪🆫🆬🆭🇦🇧🇨🇩🇪🇫🇬🇭🇮🇯🇰🇱🇲🇳🇴🇵🇶🇷🇸🇹🇺🇻🇼🇽🇾🇿🈀🈁🈂🈐🈑🈒🈓🈔🈕🈖🈗🈘🈙🈚🈛🈜🈝🈞🈟🈠🈡🈢🈣🈤🈥🈦🈧🈨🈩🈪🈫🈬🈭🈮🈯🈰🈱🈲🈳🈴🈵🈶🈷🈸🈹🈺🈻🉀🉁🉂🉃🉄🉅🉆🉇🉈🉐🉑🉠🉡🉢🉣🉤🉥🌀🌁🌂🌃🌄🌅🌆🌇🌈🌉🌊🌋🌌🌍🌎🌏🌐🌑🌒🌓🌔🌕🌖🌗🌘🌙🌚🌛🌜🌝🌞🌟🌠🌡🌢🌣🌤🌥🌦🌧🌨🌩🌪🌫🌬🌭🌮🌯🌰🌱🌲🌳🌴🌵🌶🌷🌸🌹🌺🌻🌼🌽🌾🌿🍀🍁🍂🍃🍄🍅🍆🍇🍈🍉🍊🍋🍌🍍🍎🍏🍐🍑🍒🍓🍔🍕🍖🍗🍘🍙🍚🍛🍜🍝🍞🍟🍠🍡🍢🍣🍤🍥🍦🍧🍨🍩🍪🍫🍬🍭🍮🍯🍰🍱🍲🍳🍴🍵🍶🍷🍸🍹🍺🍻🍼🍽🍾🍿🎀🎁🎂🎃🎄🎅🎆🎇🎈🎉🎊🎋🎌🎍🎎🎏🎐🎑🎒🎓🎔🎕🎖🎗🎘🎙🎚🎛🎜🎝🎞🎟🎠🎡🎢🎣🎤🎥🎦🎧🎨🎩🎪🎫🎬🎭🎮🎯🎰🎱🎲🎳🎴🎵🎶🎷🎸🎹🎺🎻🎼🎽🎾🎿🏀🏁🏂🏃🏄🏅🏆🏇🏈🏉🏊🏋🏌🏍🏎🏏🏐🏑🏒🏓🏔🏕🏖🏗🏘🏙🏚🏛🏜🏝🏞🏟🏠🏡🏢🏣🏤🏥🏦🏧🏨🏩🏪🏫🏬🏭🏮🏯🏰🏱🏲🏳🏴🏵🏶🏷🏸🏹🏺🏻🏼🏽🏾🏿🐀🐁🐂🐃🐄🐅🐆🐇🐈🐉🐊🐋🐌🐍🐎🐏🐐🐑🐒🐓🐔🐕🐖🐗🐘🐙🐚🐛🐜🐝🐞🐟🐠🐡🐢🐣🐤🐥🐦🐧🐨🐩🐪🐫🐬🐭🐮🐯🐰🐱🐲🐳🐴🐵🐶🐷🐸🐹🐺🐻🐼🐽🐾🐿👀👁👂👃👄👅👆👇👈👉👊👋👌👍👎👏👐👑👒👓👔👕👖👗👘👙👚👛👜👝👞👟👠👡👢👣👤👥👦👧👨👩👪👫👬👭👮👯👰👱👲👳👴👵👶👷👸👹👺👻👼👽👾👿💀💁💂💃💄💅💆💇💈💉💊💋💌💍💎💏💐💑💒💓💔💕💖💗💘💙💚💛💜💝💞💟💠💡💢💣💤💥💦💧💨💩💪💫💬💭💮💯💰💱💲💳💴💵💶💷💸💹💺💻💼💽💾💿📀📁📂📃📄📅📆📇📈📉📊📋📌📍📎📏📐📑📒📓📔📕📖📗📘📙📚📛📜📝📞📟📠📡📢📣📤📥📦📧📨📩📪📫📬📭📮📯📰📱📲📳📴📵📶📷📸📹📺📻📼📽📾📿🔀🔁🔂🔃🔄🔅🔆🔇🔈🔉🔊🔋🔌🔍🔎🔏🔐🔑🔒🔓🔔🔕🔖🔗🔘🔙🔚🔛🔜🔝🔞🔟🔠🔡🔢🔣🔤🔥🔦🔧🔨🔩🔪🔫🔬🔭🔮🔯🔰🔱🔲🔳🔴🔵🔶🔷🔸🔹🔺🔻🔼🔽🔾🔿🕀🕁🕂🕃🕄🕅🕆🕇🕈🕉🕊🕋🕌🕍🕎🕏🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚🕛🕜🕝🕞🕟🕠🕡🕢🕣🕤🕥🕦🕧🕨🕩🕪🕫🕬🕭🕮🕯🕰🕱🕲🕳🕴🕵🕶🕷🕸🕹🕺🕻🕼🕽🕾🕿🖀🖁🖂🖃🖄🖅🖆🖇🖈🖉🖊🖋🖌🖍🖎🖏🖐🖑🖒🖓🖔🖕🖖🖗🖘🖙🖚🖛🖜🖝🖞🖟🖠🖡🖢🖣🖤🖥🖦🖧🖨🖩🖪🖫🖬🖭🖮🖯🖰🖱🖲🖳🖴🖵🖶🖷🖸🖹🖺🖻🖼🖽🖾🖿🗀🗁🗂🗃🗄🗅🗆🗇🗈🗉🗊🗋🗌🗍🗎🗏🗐🗑🗒🗓🗔🗕🗖🗗🗘🗙🗚🗛🗜🗝🗞🗟🗠🗡🗢🗣🗤🗥🗦🗧🗨🗩🗪🗫🗬🗭🗮🗯🗰🗱🗲🗳🗴🗵🗶🗷🗸🗹🗺🗻🗼🗽🗾🗿😀😁😂😃😄😅😆😇😈😉😊😋😌😍😎😏😐😑😒😓😔😕😖😗😘😙😚😛😜😝😞😟😠😡😢😣😤😥😦😧😨😩😪😫😬😭😮😯😰😱😲😳😴😵😶😷😸😹😺😻😼😽😾😿🙀🙁🙂🙃🙄🙅🙆🙇🙈🙉🙊🙋🙌🙍🙎🙏🙐🙑🙒🙓🙔🙕🙖🙗🙘🙙🙚🙛🙜🙝🙞🙟🙠🙡🙢🙣🙤🙥🙦🙧🙨🙩🙪🙫🙬🙭🙮🙯🙰🙱🙲🙳🙴🙵🙶🙷🙸🙹🙺🙻🙼🙽🙾🙿🚀🚁🚂🚃🚄🚅🚆🚇🚈🚉🚊🚋🚌🚍🚎🚏🚐🚑🚒🚓🚔🚕🚖🚗🚘🚙🚚🚛🚜🚝🚞🚟🚠🚡🚢🚣🚤🚥🚦🚧🚨🚩🚪🚫🚬🚭🚮🚯🚰🚱🚲🚳🚴🚵🚶🚷🚸🚹🚺🚻🚼🚽🚾🚿🛀🛁🛂🛃🛄🛅🛆🛇🛈🛉🛊🛋🛌🛍🛎🛏🛐🛑🛒🛓🛔🛕🛖🛗🛜🛝🛞🛟🛠🛡🛢🛣🛤🛥🛦🛧🛨🛩🛪🛫🛬🛰🛱🛲🛳🛴🛵🛶🛷🛸🛹🛺🛻🛼🜀🜁🜂🜃🜄🜅🜆🜇🜈🜉🜊🜋🜌🜍🜎🜏🜐🜑🜒🜓🜔🜕🜖🜗🜘🜙🜚🜛🜜🜝🜞🜟🜠🜡🜢🜣🜤🜥🜦🜧🜨🜩🜪🜫🜬🜭🜮🜯🜰🜱🜲🜳🜴🜵🜶🜷🜸🜹🜺🜻🜼🜽🜾🜿🝀🝁🝂🝃🝄🝅🝆🝇🝈🝉🝊🝋🝌🝍🝎🝏🝐🝑🝒🝓🝔🝕🝖🝗🝘🝙🝚🝛🝜🝝🝞🝟🝠🝡🝢🝣🝤🝥🝦🝧🝨🝩🝪🝫🝬🝭🝮🝯🝰🝱🝲🝳🝴🝵🝶🝻🝼🝽🝾🝿🞀🞁🞂🞃🞄🞅🞆🞇🞈🞉🞊🞋🞌🞍🞎🞏🞐🞑🞒🞓🞔🞕🞖🞗🞘🞙🞚🞛🞜🞝🞞🞟🞠🞡🞢🞣🞤🞥🞦🞧🞨🞩🞪🞫🞬🞭🞮🞯🞰🞱🞲🞳🞴🞵🞶🞷🞸🞹🞺🞻🞼🞽🞾🞿🟀🟁🟂🟃🟄🟅🟆🟇🟈🟉🟊🟋🟌🟍🟎🟏🟐🟑🟒🟓🟔🟕🟖🟗🟘🟙🟠🟡🟢🟣🟤🟥🟦🟧🟨🟩🟪🟫🟰🠀🠁🠂🠃🠄🠅🠆🠇🠈🠉🠊🠋🠐🠑🠒🠓🠔🠕🠖🠗🠘🠙🠚🠛🠜🠝🠞🠟🠠🠡🠢🠣🠤🠥🠦🠧🠨🠩🠪🠫🠬🠭🠮🠯🠰🠱🠲🠳🠴🠵🠶🠷🠸🠹🠺🠻🠼🠽🠾🠿🡀🡁🡂🡃🡄🡅🡆🡇🡐🡑🡒🡓🡔🡕🡖🡗🡘🡙🡠🡡🡢🡣🡤🡥🡦🡧🡨🡩🡪🡫🡬🡭🡮🡯🡰🡱🡲🡳🡴🡵🡶🡷🡸🡹🡺🡻🡼🡽🡾🡿🢀🢁🢂🢃🢄🢅🢆🢇🢐🢑🢒🢓🢔🢕🢖🢗🢘🢙🢚🢛🢜🢝🢞🢟🢠🢡🢢🢣🢤🢥🢦🢧🢨🢩🢪🢫🢬🢭🢰🢱🤀🤁🤂🤃🤄🤅🤆🤇🤈🤉🤊🤋🤌🤍🤎🤏🤐🤑🤒🤓🤔🤕🤖🤗🤘🤙🤚🤛🤜🤝🤞🤟🤠🤡🤢🤣🤤🤥🤦🤧🤨🤩🤪🤫🤬🤭🤮🤯🤰🤱🤲🤳🤴🤵🤶🤷🤸🤹🤺🤻🤼🤽🤾🤿🥀🥁🥂🥃🥄🥅🥆🥇🥈🥉🥊🥋🥌🥍🥎🥏🥐🥑🥒🥓🥔🥕🥖🥗🥘🥙🥚🥛🥜🥝🥞🥟🥠🥡🥢🥣🥤🥥🥦🥧🥨🥩🥪🥫🥬🥭🥮🥯🥰🥱🥲🥳🥴🥵🥶🥷🥸🥹🥺🥻🥼🥽🥾🥿🦀🦁🦂🦃🦄🦅🦆🦇🦈🦉🦊🦋🦌🦍🦎🦏🦐🦑🦒🦓🦔🦕🦖🦗🦘🦙🦚🦛🦜🦝🦞🦟🦠🦡🦢🦣🦤🦥🦦🦧🦨🦩🦪🦫🦬🦭🦮🦯🦰🦱🦲🦳🦴🦵🦶🦷🦸🦹🦺🦻🦼🦽🦾🦿🧀🧁🧂🧃🧄🧅🧆🧇🧈🧉🧊🧋🧌🧍🧎🧏🧐🧑🧒🧓🧔🧕🧖🧗🧘🧙🧚🧛🧜🧝🧞🧟🧠🧡🧢🧣🧤🧥🧦🧧🧨🧩🧪🧫🧬🧭🧮🧯🧰🧱🧲🧳🧴🧵🧶🧷🧸🧹🧺🧻🧼🧽🧾🧿🨀🨁🨂🨃🨄🨅🨆🨇🨈🨉🨊🨋🨌🨍🨎🨏🨐🨑🨒🨓🨔🨕🨖🨗🨘🨙🨚🨛🨜🨝🨞🨟🨠🨡🨢🨣🨤🨥🨦🨧🨨🨩🨪🨫🨬🨭🨮🨯🨰🨱🨲🨳🨴🨵🨶🨷🨸🨹🨺🨻🨼🨽🨾🨿🩀🩁🩂🩃🩄🩅🩆🩇🩈🩉🩊🩋🩌🩍🩎🩏🩐🩑🩒🩓🩠🩡🩢🩣🩤🩥🩦🩧🩨🩩🩪🩫🩬🩭🩰🩱🩲🩳🩴🩵🩶🩷🩸🩹🩺🩻🩼🪀🪁🪂🪃🪄🪅🪆🪇🪈🪐🪑🪒🪓🪔🪕🪖🪗🪘🪙🪚🪛🪜🪝🪞🪟🪠🪡🪢🪣🪤🪥🪦🪧🪨🪩🪪🪫🪬🪭🪮🪯🪰🪱🪲🪳🪴🪵🪶🪷🪸🪹🪺🪻🪼🪽🪿🫀🫁🫂🫃🫄🫅🫎🫏🫐🫑🫒🫓🫔🫕🫖🫗🫘🫙🫚🫛🫠🫡🫢🫣🫤🫥🫦🫧🫨🫰🫱🫲🫳🫴🫵🫶🫷🫸🬀🬁🬂🬃🬄🬅🬆🬇🬈🬉🬊🬋🬌🬍🬎🬏🬐🬑🬒🬓🬔🬕🬖🬗🬘🬙🬚🬛🬜🬝🬞🬟🬠🬡🬢🬣🬤🬥🬦🬧🬨🬩🬪🬫🬬🬭🬮🬯🬰🬱🬲🬳🬴🬵🬶🬷🬸🬹🬺🬻🬼🬽🬾🬿🭀🭁🭂🭃🭄🭅🭆🭇🭈🭉🭊🭋🭌🭍🭎🭏🭐🭑🭒🭓🭔🭕🭖🭗🭘🭙🭚🭛🭜🭝🭞🭟🭠🭡🭢🭣🭤🭥🭦🭧🭨🭩🭪🭫🭬🭭🭮🭯🭰🭱🭲🭳🭴🭵🭶🭷🭸🭹🭺🭻🭼🭽🭾🭿🮀🮁🮂🮃🮄🮅🮆🮇🮈🮉🮊🮋🮌🮍🮎🮏🮐🮑🮒🮔🮕🮖🮗🮘🮙🮚🮛🮜🮝🮞🮟🮠🮡🮢🮣🮤🮥🮦🮧🮨🮩🮪🮫🮬🮭🮮🮯🮰🮱🮲🮳🮴🮵🮶🮷🮸🮹🮺🮻🮼🮽🮾🮿🯀🯁🯂🯃🯄🯅🯆🯇🯈🯉🯊"""
11
+ # https://www.compart.com/en/unicode/category
12
+ # https://unicode.org/Public/UNIDATA/UnicodeData.txt
13
+
14
+ # `\p{posix_punct}` character class
15
+ POSIX_PUNCT = r"""-!"#$%&'()*+,./:;<=>?@[\]^_`{|}~"""
16
+ ALL_PUNCT_RANGES = "".join(find_unicode_ranges(POSIX_PUNCT + UNICODE_PUNCT))
17
+ SENTENCE_PUNCT = """.?!:;'"()[-]“”·…"""
18
+
19
+
6
20
  LINKU = Path(__file__).resolve().parent / Path("linku.json")
7
21
  SANDBOX = Path(__file__).resolve().parent / Path("sandbox.json")
8
22
 
9
23
  VOWELS = "aeiou"
10
24
  CONSONANTS = "jklmnpstw"
11
25
  ALPHABET = VOWELS + CONSONANTS
12
- ALPHABET_SET = set(ALPHABET)
26
+
27
+ LANGUAGE = "english" # for NLTK
13
28
 
14
29
  """Commonly occurring strings which are some kind of valid Toki Pona or external token"""
15
30
  ALLOWABLES = {
@@ -20,48 +35,33 @@ ALLOWABLES = {
20
35
  "wxw", # wile ala wile
21
36
  }
22
37
 
23
-
24
38
  with open(LINKU) as f:
25
- r: Dict[str, Dict[str, str]] = json.loads(f.read())
26
- NIMI_PU: List[str] = [d["word"] for d in r.values() if d["book"] == "pu"]
39
+ linku: Dict[str, Dict[str, str]] = json.loads(f.read())
40
+ NIMI_PU: List[str] = [d["word"] for d in linku.values() if d["book"] == "pu"]
27
41
  NIMI_PU_ALE: List[str] = NIMI_PU + ["namako", "kin", "oko"]
28
42
  NIMI_LINKU: List[str] = [
29
- d["word"] for d in r.values() if d["usage_category"] in ["core", "common"]
43
+ d["word"] for d in linku.values() if d["usage_category"] in ["core", "common"]
30
44
  ]
31
- NIMI_LINKU_ALE: List[str] = [d["word"] for d in r.values()]
45
+ NIMI_LINKU_ALE: List[str] = [d["word"] for d in linku.values()]
32
46
 
33
47
  with open(SANDBOX) as f:
34
- r: Dict[str, Dict[str, str]] = json.loads(f.read())
35
- NIMI_LINKU_SANDBOX: List[str] = [d["word"] for d in r.values()]
48
+ sandbox: Dict[str, Dict[str, str]] = json.loads(f.read())
49
+ NIMI_LINKU_SANDBOX: List[str] = [d["word"] for d in sandbox.values()]
36
50
 
37
-
38
- NIMI_PU_SET = set(NIMI_PU)
39
- NIMI_PU_ALE_SET = set(NIMI_PU_ALE)
40
- NIMI_LINKU_SET = set(NIMI_LINKU)
41
- NIMI_LINKU_ALE_SET = set(NIMI_LINKU_ALE)
42
- NIMI_LINKU_SANDBOX_SET = set(NIMI_LINKU_SANDBOX)
43
- ALLOWABLES_SET = set(ALLOWABLES)
51
+ del linku
52
+ del sandbox
44
53
 
45
54
  __all__ = [
46
- "VOWELS",
47
- #
48
- "CONSONANTS",
49
- #
50
55
  "ALPHABET",
51
- "ALPHABET_SET",
52
- #
53
- "NIMI_PU",
54
- "NIMI_PU_SET",
55
- #
56
- "NIMI_PU_ALE",
57
- "NIMI_PU_ALE_SET",
58
- #
56
+ "CONSONANTS",
59
57
  "NIMI_LINKU",
60
- "NIMI_LINKU_SET",
61
- #
62
58
  "NIMI_LINKU_ALE",
63
- "NIMI_LINKU_ALE_SET",
64
- #
65
59
  "NIMI_LINKU_SANDBOX",
66
- "NIMI_LINKU_SANDBOX_SET",
60
+ "NIMI_PU",
61
+ "NIMI_PU_ALE",
62
+ "VOWELS",
63
+ "UNICODE_PUNCT",
64
+ "ALLOWABLES",
65
+ "POSIX_PUNCT",
66
+ "",
67
67
  ]
sonatoki/ilo.py CHANGED
@@ -1,5 +1,4 @@
1
1
  # STL
2
- import logging
3
2
  from typing import List, Type, Tuple
4
3
 
5
4
  # LOCAL
@@ -9,8 +8,6 @@ from sonatoki.Cleaners import Cleaner
9
8
  from sonatoki.Tokenizers import Tokenizer
10
9
  from sonatoki.Preprocessors import Preprocessor
11
10
 
12
- LOG = logging.getLogger(__name__)
13
-
14
11
 
15
12
  class Ilo:
16
13
  __preprocessors: List[Type[Preprocessor]]
@@ -20,7 +17,6 @@ class Ilo:
20
17
  __scoring_filters: List[Type[Filter]]
21
18
  __scorer: Type[Scorer]
22
19
  __passing_score: Number
23
- logging_threshold: Number = -1
24
20
 
25
21
  def __init__(
26
22
  self,
@@ -104,14 +100,6 @@ class Ilo:
104
100
  score = self.score_tokens(cleaned)
105
101
  result = score >= self.__passing_score
106
102
 
107
- if score <= self.logging_threshold:
108
- LOG.debug("msg: %.2f %s", score, repr(message))
109
- LOG.debug("preproc: %s", repr(preprocessed))
110
- LOG.debug("tokenized: %s", tokenized)
111
- LOG.debug("filtered: %s", filtered)
112
- LOG.debug("cleaned: %s", cleaned)
113
- # TODO: Move to each function? Loses ability to control when logging occurs by threshold
114
-
115
103
  return preprocessed, tokenized, filtered, cleaned, score, result
116
104
 
117
105
  def is_toki_pona(self, message: str) -> bool:
sonatoki/utils.py ADDED
@@ -0,0 +1,90 @@
1
+ # STL
2
+ import re
3
+ from typing import List
4
+
5
+ TO_ESCAPE = ["^", "]", "\\"]
6
+
7
+
8
+ def regex_escape(s: str) -> str:
9
+ """Escape all characters which must be escaped when embedded in a character class."""
10
+ for c in TO_ESCAPE:
11
+ s = s.replace(c, f"\\{c}") # one backslash
12
+ return s
13
+
14
+
15
+ def to_range(start: int, prev: int) -> str:
16
+ if start == prev:
17
+ return rf"\U{start:08x}"
18
+ return rf"\U{start:08x}-\U{prev:08x}"
19
+
20
+
21
+ def find_unicode_ranges(chars: str) -> List[str]:
22
+ if not chars:
23
+ return []
24
+
25
+ s_chars = sorted(set(chars))
26
+
27
+ ranges: List[str] = []
28
+ start = ord(s_chars[0])
29
+ prev = start
30
+
31
+ for i in range(1, len(s_chars)):
32
+ cur = ord(s_chars[i])
33
+ if cur == prev + 1: # range is still contiguous
34
+ prev = cur
35
+ continue
36
+
37
+ ranges.append(to_range(start, prev))
38
+ start = prev = cur
39
+
40
+ last = ord(s_chars[-1])
41
+ ranges.append(to_range(start, last))
42
+
43
+ return ranges
44
+
45
+
46
+ if __name__ == "__main__":
47
+ """
48
+ Helper script to fetch UNICODE_PUNCT in constants.py
49
+ """
50
+
51
+ PUNCT_CATEGORIES = {"Pc", "Pd", "Pe", "Pf", "Pi", "Po", "Ps", "Sm", "Sk", "So"}
52
+ # Connector, Dash, Close (end), Final, Initial, Other, Open (sOpen), Symbol math, Symbol kmodifier, Symbol other
53
+
54
+ # NOTE: UnicodeData.txt lists character ranges if there would be many characters.
55
+ # (e.g. CJK Ideograph, First at 4E00 and CJK Ideograph, Last at 9FFF).
56
+ # This does not apply to any currently defined punctuation category.
57
+
58
+ EXCEPTION_RANGES = re.compile(r"""[Ⓐ-ⓩ🄰-🅉🅐-🅩🅰-🆉]+""")
59
+ # These groups are in Symbol other (So) but are not part of `\p{Punctuation}`
60
+ # NOTE: There are many characters which look like writing characters but are not. Examples:
61
+ # - kangxi radicals from ⺀ to ⿕ which are for demonstration
62
+ # - circled katakana from to ㋾ which... shouldn't be in \p{Punctuation} but oh well
63
+
64
+ def is_punctuation(data: List[str]):
65
+ return data[2] in PUNCT_CATEGORIES
66
+
67
+ def get_character(data: List[str]):
68
+ return chr(int(data[0], 16))
69
+
70
+ def is_exception(c: str):
71
+ return not not re.fullmatch(EXCEPTION_RANGES, c)
72
+
73
+ # http://www.unicode.org/Public/UNIDATA/UnicodeData.txt
74
+ #
75
+
76
+ unicode_punctuation = ""
77
+ with open("UnicodeData.txt", "r") as f:
78
+ for line in f:
79
+ data = line.split(";")
80
+ if not is_punctuation(data):
81
+ continue
82
+
83
+ char = get_character(data)
84
+ if is_exception(char):
85
+ continue
86
+
87
+ unicode_punctuation += char
88
+
89
+ with open("UnicodePunctuation.txt", "w") as f:
90
+ _ = f.write(unicode_punctuation)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sonatoki
3
- Version: 0.1.4
3
+ Version: 0.2.0
4
4
  Summary: ilo li moku e toki li pana e sona ni: ni li toki ala toki pona?
5
5
  Author-Email: "jan Kekan San (@gregdan3)" <gregory.danielson3@gmail.com>
6
6
  License: AGPL-3.0-or-later
@@ -8,8 +8,6 @@ Requires-Python: >=3.8
8
8
  Requires-Dist: unidecode>=1.3.6
9
9
  Requires-Dist: regex>=2023.12.25
10
10
  Requires-Dist: typing-extensions>=4.11.0
11
- Requires-Dist: nltk>=3.8.1; extra == "nltk"
12
- Provides-Extra: nltk
13
11
  Description-Content-Type: text/markdown
14
12
 
15
13
  # sona toki
@@ -0,0 +1,17 @@
1
+ sonatoki-0.2.0.dist-info/METADATA,sha256=9xz_hA65WMEka6qTXhY6oGAoElV17rkSd9WhDSxL4D0,5160
2
+ sonatoki-0.2.0.dist-info/WHEEL,sha256=vnE8JVcI2Wz7GRKorsPArnBdnW2SWKWGow5gu5tHlRU,90
3
+ sonatoki-0.2.0.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
4
+ sonatoki/Cleaners.py,sha256=AMonXBUk3w1vdRiDrpB9XJAdjYaMPoqRtdX5oLI6r38,1744
5
+ sonatoki/Configs.py,sha256=5mucu-Zsnt2p7GMiaM7GXUeL1F1fBq9sycjm4V7xsrI,1929
6
+ sonatoki/Filters.py,sha256=hZfVVv2e4ig_5hM2hdCsdNi21CFFK_AT53oO4N4H6FU,5276
7
+ sonatoki/Preprocessors.py,sha256=aMXXuFBDlJudvzvukvCa7BixuROXXEb62un7I-TGOGs,4441
8
+ sonatoki/Scorers.py,sha256=W-1uYiqjsDejJzoe592ixs7wHazjJXPhuo-41zuJ26U,3643
9
+ sonatoki/Tokenizers.py,sha256=zJ_5h9dlDIiJlLc6inuiOodWYt52nD83wS0QwSZixiM,3326
10
+ sonatoki/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ sonatoki/__main__.py,sha256=6xc-wIrrFo9wTyn4zRQNAmqwmJBtVvCMwV-CrM-hueA,82
12
+ sonatoki/constants.py,sha256=wgNn7wyy75_HWV-zHhmDHGQT7RncHbZHonnaDFJJVe8,30942
13
+ sonatoki/ilo.py,sha256=yyLgNPI0Hmb4f1BzX6IRHr11FPChfL2xDR_9odlr8_8,3849
14
+ sonatoki/linku.json,sha256=B5KNdhyM5UEfMciROgh1ECHr3i-ASBeMvwrkzNJX47c,271013
15
+ sonatoki/sandbox.json,sha256=hx6LRsfvmmTtqXcXIyCsfSaGK3DZ-GCdbM8xhZQBHoA,77650
16
+ sonatoki/utils.py,sha256=cYxT8W6LcRWFTrtTAb7LgxClcjPk0b6lvbTit3lVDS8,2660
17
+ sonatoki-0.2.0.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- sonatoki-0.1.4.dist-info/METADATA,sha256=cK_EyYXPeY4rm9Plcre-i_DbPJZD06572cYQEIUQ804,5225
2
- sonatoki-0.1.4.dist-info/WHEEL,sha256=vnE8JVcI2Wz7GRKorsPArnBdnW2SWKWGow5gu5tHlRU,90
3
- sonatoki-0.1.4.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
4
- sonatoki/Cleaners.py,sha256=gTZ9dSsnvKVUtxM_ECSZ-_2heh--nD5A9dCQR1ATb1c,1160
5
- sonatoki/Configs.py,sha256=iY6Lyn1rMi7iF0M62yx0ET4pEb35-QAd1FS0tkyUfSc,1935
6
- sonatoki/Filters.py,sha256=dL3XgH62OrVVvc8b6dtR5-JZmErVF4bl7ultAoHHqpo,4190
7
- sonatoki/Preprocessors.py,sha256=h2sX6nJIIOPotwHL0476VQe4KxERlD_F6nrvxDyuaTs,4205
8
- sonatoki/Scorers.py,sha256=V293DBiupBiujzuc4yMrKOAiuNTLltIsiCzIAlLeokA,4129
9
- sonatoki/Tokenizers.py,sha256=fvqxpubs2F63va2RzZKZQhZbFnVaC_9haXIA9Mqznis,1942
10
- sonatoki/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- sonatoki/__main__.py,sha256=6xc-wIrrFo9wTyn4zRQNAmqwmJBtVvCMwV-CrM-hueA,82
12
- sonatoki/constants.py,sha256=m0Z4At6MfbqZRio2glT3J3zT9x_itcWZBT_G82mpaVc,1647
13
- sonatoki/ilo.py,sha256=oN14iYFKxgjFjjOslgqBrMaIgpnvS5gO6MscbS0JS5A,4343
14
- sonatoki/linku.json,sha256=B5KNdhyM5UEfMciROgh1ECHr3i-ASBeMvwrkzNJX47c,271013
15
- sonatoki/sandbox.json,sha256=hx6LRsfvmmTtqXcXIyCsfSaGK3DZ-GCdbM8xhZQBHoA,77650
16
- sonatoki-0.1.4.dist-info/RECORD,,