1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package social.grain
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import social.grain.util.GrainLimits
import social.grain.util.TextLimit
import social.grain.util.graphemeCount
import social.grain.util.utf8ByteCount
/**
* On the device rather than the JVM: grapheme segmentation comes from ICU, and
* the JVM's own BreakIterator gives different — wrong — answers for flags and
* ZWJ sequences.
*/
@RunWith(AndroidJUnit4::class)
class TextLimitsTest {
@Test
fun countsPlainTextOnePerCharacter() {
assertEquals(5, "hello".graphemeCount())
assertEquals(5, "hello".utf8ByteCount())
}
@Test
fun countsAnAccentedCharacterAsOneGraphemeAndTwoBytes() {
assertEquals(4, "café".graphemeCount())
assertEquals(5, "café".utf8ByteCount())
}
/** The case String.length gets wrong: one glyph, two UTF-16 code units. */
@Test
fun countsAnEmojiAsOneGrapheme() {
assertEquals(2, "📷".length)
assertEquals(1, "📷".graphemeCount())
assertEquals(4, "📷".utf8ByteCount())
}
/** A flag is two regional indicators; a reader sees one flag. */
@Test
fun countsAFlagAsOneGrapheme() {
assertEquals(1, "🇬🇧".graphemeCount())
assertEquals(8, "🇬🇧".utf8ByteCount())
}
@Test
fun countsAZwjSequenceAsOneGrapheme() {
assertEquals(1, "👩👩👧".graphemeCount())
}
@Test
fun emptyTextCountsZero() {
assertEquals(0, "".graphemeCount())
assertEquals(0, "".utf8ByteCount())
}
@Test
fun aGalleryTitleOfPlainTextIsBoundedByItsGraphemeCount() {
assertTrue(GrainLimits.GalleryTitle.isWithin("a".repeat(100)))
assertFalse(GrainLimits.GalleryTitle.isWithin("a".repeat(101)))
}
/**
* The reason both units are checked. Twenty-six emoji is well inside the
* gallery title's 100, but 104 bytes — over the cap the PDS enforces.
*/
@Test
fun aGalleryTitleWithinItsGraphemeCountCanStillBeOverInBytes() {
val emoji = "📷".repeat(26)
assertEquals(26, emoji.graphemeCount())
assertEquals(104, emoji.utf8ByteCount())
assertFalse(GrainLimits.GalleryTitle.isWithin(emoji))
}
/**
* The profile lexicon gives itself ten bytes per grapheme, so for
* displayName the grapheme count is what binds — 64 emoji is 256 bytes,
* comfortably inside 640.
*/
@Test
fun aDisplayNameIsBoundedByGraphemesRatherThanBytes() {
val emoji = "📷".repeat(64)
assertEquals(256, emoji.utf8ByteCount())
assertTrue(GrainLimits.DisplayName.isWithin(emoji))
assertFalse(GrainLimits.DisplayName.isWithin("📷".repeat(65)))
}
@Test
fun limitsMatchTheLexicons() {
assertEquals(TextLimit(graphemes = 100, bytes = 100), GrainLimits.GalleryTitle)
assertEquals(TextLimit(graphemes = 1000, bytes = 1000), GrainLimits.GalleryDescription)
assertEquals(TextLimit(graphemes = 64, bytes = 640), GrainLimits.DisplayName)
assertEquals(TextLimit(graphemes = 256, bytes = 2560), GrainLimits.Bio)
}
}