Skip to content

Instantly share code, notes, and snippets.

@kafri8889
Last active May 30, 2023 08:33
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save kafri8889/3e0f6c24bd024f0a0741ff59da22942e to your computer and use it in GitHub Desktop.
Save kafri8889/3e0f6c24bd024f0a0741ff59da22942e to your computer and use it in GitHub Desktop.
Date format in TextField Jetpack Compose
@Composable
fun Screen() {
var date by remember {
mutableStateOf(
TextFieldValue(
text = "dd-MM-yyyy"
)
)
}
DateTextField(
value = date,
onValueChange = {
date = it
}
)
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun DateTextField(
value: TextFieldValue,
modifier: Modifier = Modifier,
onValueChange: (TextFieldValue) -> Unit
) {
var isBackspacePressed by remember { mutableStateOf(false) }
OutlinedTextField(
value = value,
visualTransformation = DateFormatVisualTransformation(LocalTextStyle.current),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done
),
onValueChange = {
// ex: "01-1M-yyyy" -> "011"
val date = it.text.takeDigitString()
if (date.length < 9) {
// if date length is 3 or 5, move cursor to index + 1
// ex: (| = cursor) "01-|1M-yyyy" to "01-1|M-yyyy"
val selection = if (!isBackspacePressed) {
when (date.length) {
3, 5 -> it.selection.start + 1
else -> it.selection.start
}
} else it.selection.start
onValueChange(
it.copy(
text = TextFieldDateFormatter.format(it),
selection = TextRange(selection)
)
)
}
},
modifier = modifier
.onKeyEvent { event ->
if (event.key == Key.Backspace) {
isBackspacePressed = true
return@onKeyEvent true
} else {
isBackspacePressed = false
}
false
}
)
}
class DateFormatVisualTransformation(
private val textStyle: TextStyle
): VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
return TransformedText(
text = buildAnnotatedString {
for (word in text) {
withStyle(
textStyle.copy(
color = if (word.isDigit() || word == '-') textStyle.color
else Color.Gray
).toSpanStyle()
) {
append(word)
}
}
},
offsetMapping = OffsetMapping.Identity
)
}
}
object TextFieldDateFormatter {
private const val ddMMyyyy = "ddMMyyyy"
/**
* format "ddMMyyyy" to "dd-MMM-yyyy"
* @return formatted string, ex: "11-01-2007" or "11-0M-YYYY"
*/
fun format(
fieldValue: TextFieldValue,
minYear: Int = 2000,
maxYear: Int = 3000
): String {
val builder = StringBuilder()
val s = fieldValue.text.replace(
oldValue = listOf(" ", ".", ",", "-", "d", "M", "y"),
newValue = ""
)
if (s.length != 8) {
for (i in 0 until 8) {
builder.append(
try {
s[i]
} catch (e: Exception) {
ddMMyyyy[i]
}
)
}
} else builder.append(s)
repeat(3) {
when (it) {
0 -> {
val day = try {
"${builder[0]}${builder[1]}".toInt()
} catch (e: Exception) {
null
}
if (day != null) {
val dayMax = day
.coerceIn(
minimumValue = 1,
maximumValue = 31,
)
.toString()
builder.replace(
0,
2,
if (dayMax.length == 1) "0$dayMax" else dayMax
)
}
}
1 -> {
val month = try {
"${builder[2]}${builder[3]}".toInt()
} catch (e: Exception) {
null
}
if (month != null) {
val monthMax = month
.coerceIn(
minimumValue = 1,
maximumValue = 12,
)
.toString()
builder.replace(
2,
4,
if (monthMax.length == 1) "0$monthMax" else monthMax
)
}
}
2 -> {
val year = try {
"${builder[4]}${builder[5]}${builder[6]}${builder[7]}".toInt()
} catch (e: Exception) {
null
}
if (year != null) {
val yearMaxMin = year.coerceIn(
minimumValue = minYear,
maximumValue = maxYear
).toString()
builder.replace(4, 6, yearMaxMin.substring(0, 2))
builder.replace(6, 8, yearMaxMin.substring(2, 4))
}
}
}
}
return builder.toString()
.addStringBefore("-", 2) // dd-MMyyyy
.addStringBefore("-", 5) // dd-MM-yyyy
}
/**
* format time in milli second to formatted date
* @return formatted string, ex: "11-01-2007"
*/
fun format(timeInMillis: Long): String {
return SimpleDateFormat("dd-MM-yyyy", deviceLocale).format(timeInMillis)
}
/**
* check if formattedDate is valid
*
* "01-01-2000" -> valid
* "01-0M-YYYY" -> not valid
* @param formattedDate "01-01-2000"
*/
fun isValid(formattedDate: String): Boolean {
return formattedDate.takeDigitString().length == 8
}
/**
* convert formatted string date to time in millis
* @param formattedDate "01-01-2000"
* @return time in millis
*/
fun parse(formattedDate: String): Long {
val date = "${formattedDate[6]}${formattedDate[7]}${formattedDate[8]}${formattedDate[9]}-"
.plus("${formattedDate[3]}${formattedDate[4]}-")
.plus("${formattedDate[0]}${formattedDate[1]}")
.plus("T00:00")
return LocalDateTime.parse(date)
.atZone(ZoneId.of(TimeZone.getDefault().id, ZoneId.SHORT_IDS))
.toInstant()
.toEpochMilli()
}
}
/**
* take a string that is a number
*
* ex:
* val s = "123a456b"
* s.takeDigitString() => "123456"
* @author kafri8889
*/
fun String.takeDigitString(): String {
var builder = ""
forEach { if (it.isDigit()) builder += it }
return builder
}
/**
* add a new string before the given index
* @author kafri8889
*/
fun String.addStringBefore(s: String, index: Int): String {
val result = StringBuilder()
forEachIndexed { i, c ->
if (i == index) {
result.append(s)
result.append(c)
} else result.append(c)
}
return result.toString()
}
/**
* Returns a new string with all occurrences of oldValue replaced with newValue.
* @author kafri8889
*/
fun String.replace(
oldValue: List<String>,
newValue: String,
ignoreCase: Boolean = false
): String {
var result = this
oldValue.forEach { s ->
if (this.contains(s)) {
result = result.replace(s, newValue, ignoreCase)
}
}
return result
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment