Skip to content

Commit

Permalink
Fix issues in expiry date visual transformation (#6411)
Browse files Browse the repository at this point in the history
* Fix issues in expiry date visual transformation

* Update ExpiryDateVisualTransformationTest

* Update detekt baseline
  • Loading branch information
tillh-stripe authored Mar 27, 2023
1 parent 475f055 commit ece01bb
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 35 deletions.
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() }
}
}
}

0 comments on commit ece01bb

Please sign in to comment.