Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issues in expiry date visual transformation #6411

Merged
merged 3 commits into from
Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,34 +1,64 @@
package com.stripe.android.ui.core.elements

import androidx.compose.ui.text.AnnotatedString
import com.google.common.truth.Truth
import androidx.compose.ui.text.input.TransformedText
import com.google.common.truth.Truth.assertThat
import com.stripe.android.uicore.elements.ExpiryDateVisualTransformation
import org.junit.Test

internal class ExpiryDateVisualTransformationTest {

private val transform = ExpiryDateVisualTransformation()

@Test
fun `verify 19 get separated between 1 and 9`() {
Truth.assertThat(transform.filter(AnnotatedString("19")).text.text)
.isEqualTo("1 / 9")
val result = transform.filter(AnnotatedString("19"))
assertThat(result.text.text).isEqualTo("1 / 9")
assertCorrectMapping(original = "19", result)
}

@Test
fun `verify 123 get separated between 2 and 3`() {
Truth.assertThat(transform.filter(AnnotatedString("123")).text.text)
.isEqualTo("12 / 3")
val result = transform.filter(AnnotatedString("123"))
assertThat(result.text.text).isEqualTo("12 / 3")
assertCorrectMapping(original = "123", result)
}

@Test
fun `verify 143 get separated between 1 and 4`() {
val result = transform.filter(AnnotatedString("143"))
assertThat(result.text.text).isEqualTo("1 / 43")
assertCorrectMapping(original = "143", result)
}

@Test
fun `verify 093 get separated between 9 and 3`() {
Truth.assertThat(transform.filter(AnnotatedString("093")).text.text)
.isEqualTo("09 / 3")
val result = transform.filter(AnnotatedString("093"))
assertThat(result.text.text).isEqualTo("09 / 3")
assertCorrectMapping(original = "093", result)
}

@Test
fun `verify 53 get separated between 5 and 3`() {
Truth.assertThat(transform.filter(AnnotatedString("53")).text.text)
.isEqualTo("5 / 3")
val result = transform.filter(AnnotatedString("53"))
assertThat(result.text.text).isEqualTo("5 / 3")
assertCorrectMapping(original = "53", result)
}

private fun assertCorrectMapping(
original: String,
result: TransformedText,
) {
val transformed = result.text.text

for (offset in 0..original.length) {
val transformedOffset = result.offsetMapping.originalToTransformed(offset)
assertThat(transformedOffset).isIn(0..transformed.length)
}

for (offset in 0..result.text.text.length) {
val originalOffset = result.offsetMapping.transformedToOriginal(offset)
assertThat(originalOffset).isIn(0..original.length)
}
}
}
1 change: 1 addition & 0 deletions stripe-ui-core/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<ID>MagicNumber:DropdownFieldUI.kt$.8f</ID>
<ID>MagicNumber:DropdownFieldUI.kt$.9f</ID>
<ID>MagicNumber:DropdownFieldUI.kt$8.9f</ID>
<ID>MagicNumber:ExpiryDateVisualTransformation.kt$ExpiryDateVisualTransformation$12</ID>
<ID>MagicNumber:Html.kt$0.1f</ID>
<ID>MagicNumber:PostalCodeVisualTransformation.kt$PostalCodeVisualTransformation.&lt;no name provided>$3</ID>
<ID>MagicNumber:PostalCodeVisualTransformation.kt$PostalCodeVisualTransformation.&lt;no name provided>$5</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,39 +17,54 @@ class ExpiryDateVisualTransformation : VisualTransformation {
* 2, if the first number is 11 or 12 it will be after the second digit,
* if the number is 01 it will be after the second digit.
*/
var separatorAfterIndex = 1
if (text.isNotBlank() && !(text[0] == '0' || text[0] == '1')) {
separatorAfterIndex = 0
} else if (text.length > 1 &&
(text[0] == '1' && requireNotNull(text[1].digitToInt()) > 2)
) {
separatorAfterIndex = 0
}
val canOnlyBeSingleDigitMonth = text.isNotBlank() && !(text[0] == '0' || text[0] == '1')
val canOnlyBeJanuary = text.length > 1 && text.text.take(2).toInt() > 12
val isSingleDigitMonth = canOnlyBeSingleDigitMonth || canOnlyBeJanuary

val lastIndexOfMonth = if (isSingleDigitMonth) 0 else 1

var out = ""
for (i in text.indices) {
out += text[i]
if (i == separatorAfterIndex) {
out += separator
val output = buildString {
for ((index, char) in text.withIndex()) {
append(char)
if (index == lastIndexOfMonth) {
append(separator)
}
}
}

val outputOffsets = calculateOutputOffsets(output)
val separatorIndices = calculateSeparatorOffsets(output)

val offsetTranslator = object : OffsetMapping {
override fun originalToTransformed(offset: Int) =
if (offset <= separatorAfterIndex) {
offset
} else {
offset + separator.length
}

override fun transformedToOriginal(offset: Int) =
if (offset <= separatorAfterIndex + 1) {
offset
} else {
offset - separator.length
}
override fun originalToTransformed(offset: Int): Int {
return outputOffsets[offset]
}

override fun transformedToOriginal(offset: Int): Int {
val separatorCharactersBeforeOffset = separatorIndices.count { it < offset }
return offset - separatorCharactersBeforeOffset
}
}

return TransformedText(AnnotatedString(output), offsetTranslator)
}

private fun calculateOutputOffsets(output: String): List<Int> {
val digitOffsets = output.mapIndexedNotNull { index, char ->
// +1 because we're looking for offsets, not indices
index.takeIf { char.isDigit() }?.plus(1)
}

return TransformedText(AnnotatedString(out), offsetTranslator)
// We're adding 0 so that the cursor can be placed at the start of the text,
// and replace the last digit offset with the length of the output. The latter
// is so that the offsets are set correctly for text such as "4 / ".
return listOf(0) + digitOffsets.dropLast(1) + output.length
}

private fun calculateSeparatorOffsets(output: String): List<Int> {
return output.mapIndexedNotNull { index, c ->
index.takeUnless { c.isDigit() }
}
}
}