diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c528899..38fdeb9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,11 @@ android { applicationId = "com.racingapp.pitlog" minSdk = 26 targetSdk = 36 - versionCode = 3 - versionName = "0.003" + // Three digits. versionCode is that number with the dot removed. + // A fix adds 0.001. One feature adds 0.010. A batch of features adds 0.100. + // The release tag is v plus versionName, such as v0.013. + versionCode = 23 + versionName = "0.023" } buildTypes { diff --git a/app/src/main/java/com/racingapp/pitlog/model/Parsing.kt b/app/src/main/java/com/racingapp/pitlog/model/Parsing.kt index 8d6af9b..1813913 100644 --- a/app/src/main/java/com/racingapp/pitlog/model/Parsing.kt +++ b/app/src/main/java/com/racingapp/pitlog/model/Parsing.kt @@ -1,6 +1,8 @@ package com.racingapp.pitlog.model import java.util.Locale +import kotlin.math.abs +import kotlin.math.roundToInt fun sanitizeSignedDecimal(raw: String, wholeDigits: Int, fractionDigits: Int): String { val negative = raw.trimStart().startsWith('-') @@ -31,6 +33,72 @@ fun parseDecimal(raw: String): Double? { return trimmed.toDoubleOrNull() } +fun sanitizeInches(raw: String, signed: Boolean = false): String { + val negative = signed && raw.trimStart().startsWith('-') + val source = raw.trim().removePrefix("-").removePrefix("+").replace(',', '.') + val out = StringBuilder() + var slash = false + var dot = false + var space = false + var digits = 0 + for (ch in source) { + when { + ch.isDigit() -> { + val limit = when { + slash || dot -> if (dot) 4 else 2 + else -> 2 + } + if (digits < limit) { + out.append(ch) + digits++ + } + } + ch == '/' && !slash && !dot && out.isNotEmpty() && !out.endsWith(' ') -> { + slash = true + digits = 0 + out.append('/') + } + ch == '.' && !dot && !slash && !space -> { + dot = true + digits = 0 + out.append('.') + } + ch == ' ' && !space && !slash && !dot && digits > 0 -> { + space = true + digits = 0 + out.append(' ') + } + } + } + val text = out.toString() + if (text.isEmpty()) return if (negative) "-" else "" + return if (negative) "-$text" else text +} + +fun parseInches(raw: String): Double? { + val trimmed = raw.trim().removeSuffix(" ") + if (trimmed.isEmpty() || trimmed == "-" || trimmed.endsWith("/") || trimmed.endsWith(".")) return null + val negative = trimmed.startsWith("-") + val body = trimmed.removePrefix("-").trim() + val value = if (body.contains("/")) parseFraction(body) else parseDecimal(body) + value ?: return null + return if (negative) -value else value +} + +fun formatInches(value: Double?): String { + if (value == null) return "" + val negative = value < 0 + val magnitude = abs(value) + val scaled = magnitude * 64.0 + val nearest = scaled.roundToInt() + val body = if (abs(scaled - nearest) <= 0.04) { + fractionInches(nearest) + } else { + String.format(Locale.US, "%.3f", magnitude).trimEnd('0').trimEnd('.') + } + return if (negative && body != "0") "-$body" else body +} + fun parseIntField(raw: String): Int? = raw.trim().toIntOrNull() fun formatPsi(value: Double?): String = @@ -52,15 +120,48 @@ fun formatDegrees(value: Double?): String = fun formatStagger(value: Double?): String { if (value == null) return "—" - return String.format(Locale.US, "%.2f", value).trimEnd('0').trimEnd('.') + return formatInches(value) } fun psiField(value: Double?): String = if (value == null) "" else String.format(Locale.US, "%.1f", value) -fun staggerField(value: Double?): String { - if (value == null) return "" - return String.format(Locale.US, "%.2f", value).trimEnd('0').trimEnd('.') +fun staggerField(value: Double?): String = formatInches(value) + +private fun parseFraction(body: String): Double? { + val parts = body.split(' ').filter { it.isNotEmpty() } + val whole: Double + val token: String + when (parts.size) { + 1 -> { + whole = 0.0 + token = parts[0] + } + 2 -> { + whole = parts[0].toDoubleOrNull() ?: return null + token = parts[1] + } + else -> return null + } + val bits = token.split('/') + if (bits.size != 2 || bits[1].isEmpty()) return null + val numerator = bits[0].toDoubleOrNull() ?: return null + val denominator = bits[1].toDoubleOrNull() ?: return null + if (denominator == 0.0) return null + return whole + numerator / denominator +} + +private fun fractionInches(sixtyFourths: Int): String { + val whole = sixtyFourths / 64 + var numerator = sixtyFourths % 64 + var denominator = 64 + if (numerator == 0) return whole.toString() + while (numerator % 2 == 0) { + numerator /= 2 + denominator /= 2 + } + val fraction = "$numerator/$denominator" + return if (whole == 0) fraction else "$whole $fraction" } fun intField(value: Int?): String = value?.toString().orEmpty() diff --git a/app/src/main/java/com/racingapp/pitlog/model/RunForm.kt b/app/src/main/java/com/racingapp/pitlog/model/RunForm.kt index f4c2000..19803d1 100644 --- a/app/src/main/java/com/racingapp/pitlog/model/RunForm.kt +++ b/app/src/main/java/com/racingapp/pitlog/model/RunForm.kt @@ -123,7 +123,7 @@ data class RunForm( if (sheet.cold.any() || sheet.hot.any()) return true if (sheet.heat.values.any { it.any() }) return true if (parseIntField(laps) != null || parseIntField(finish) != null) return true - if (parseDecimal(stagger) != null) return true + if (parseInches(stagger) != null) return true if (tireSet.isNotBlank() || notes.isNotBlank()) return true return handling != null || handlingWhere != null } @@ -137,7 +137,7 @@ data class RunForm( tireSet = tireSet.trim(), laps = parseIntField(laps), finish = parseIntField(finish), - staggerInches = parseDecimal(stagger), + staggerInches = parseInches(stagger), coldLf = sheet.cold.lf, coldRf = sheet.cold.rf, coldLr = sheet.cold.lr, diff --git a/app/src/main/java/com/racingapp/pitlog/model/SetupLog.kt b/app/src/main/java/com/racingapp/pitlog/model/SetupLog.kt index b161cb9..94d3ae7 100644 --- a/app/src/main/java/com/racingapp/pitlog/model/SetupLog.kt +++ b/app/src/main/java/com/racingapp/pitlog/model/SetupLog.kt @@ -28,22 +28,61 @@ fun setupLines(sheet: SetupSheet): List = buildList { "RR" to sheet.weightRr, ).mapNotNull { (corner, pounds) -> pounds?.let { "$corner $it" } } if (weights.isNotEmpty()) add("Weights ${weights.joinToString(" ")} lb") - weightSplit(sheet.weightLf, sheet.weightRf, sheet.weightLr, sheet.weightRr)?.let { add("From the weights $it") } + addAll(scaleLines(sheet.weightLf, sheet.weightRf, sheet.weightLr, sheet.weightRr)) sheet.rideHeight?.let { add("Ride height ${inches(it)} in") } sheet.pinion?.let { add("Pinion ${formatDegrees(it)}°") } sheet.brakeFront?.let { add("Front brake ${onePlace(it)}%") } } -fun weightSplit(lf: Int?, rf: Int?, lr: Int?, rr: Int?): String? { +fun scaleLines(lf: Int?, rf: Int?, lr: Int?, rr: Int?): List { + val reading = scaleReading(lf, rf, lr, rr) ?: return emptyList() + return listOf( + "Total ${reading.total} lb", + "Left ${pair(reading.left, reading.leftPercent)}", + "Front ${pair(reading.front, reading.frontPercent)}", + "Rear ${pair(reading.rear, reading.rearPercent)}", + "Cross ${pair(reading.cross, reading.crossPercent)}", + "Rear bite ${reading.rearBite}", + ) +} + +private data class ScaleReading( + val total: Int, + val left: Int, + val leftPercent: Double, + val front: Int, + val frontPercent: Double, + val rear: Int, + val rearPercent: Double, + val cross: Int, + val crossPercent: Double, + val rearBite: Int, +) + +private fun scaleReading(lf: Int?, rf: Int?, lr: Int?, rr: Int?): ScaleReading? { if (lf == null || rf == null || lr == null || rr == null) return null val total = lf + rf + lr + rr if (total <= 0) return null - val left = (lf + lr) * 100.0 / total - val rear = (lr + rr) * 100.0 / total - val cross = (rf + lr) * 100.0 / total - return "Left ${onePlace(left)}% Rear ${onePlace(rear)}% Cross ${onePlace(cross)}%" + val left = lf + lr + val front = lf + rf + val rear = lr + rr + val cross = rf + lr + return ScaleReading( + total = total, + left = left, + leftPercent = left * 100.0 / total, + front = front, + frontPercent = front * 100.0 / total, + rear = rear, + rearPercent = rear * 100.0 / total, + cross = cross, + crossPercent = cross * 100.0 / total, + rearBite = lr - rr, + ) } +private fun pair(pounds: Int, percent: Double) = "$pounds lb, ${onePlace(percent)}%" + fun setupDiffs(before: SetupSheet, after: SetupSheet): List = buildList { changed(before.crossPercent, after.crossPercent, ::percent)?.let { add("Cross $it") } changedInt(before.springLf, after.springLf, " lb")?.let { add("LF spring $it") } @@ -63,18 +102,40 @@ fun setupDiffs(before: SetupSheet, after: SetupSheet): List = buildList changedInt(before.weightRf, after.weightRf, " lb")?.let { add("RF weight $it") } changedInt(before.weightLr, after.weightLr, " lb")?.let { add("LR weight $it") } changedInt(before.weightRr, after.weightRr, " lb")?.let { add("RR weight $it") } + addAll(scaleChanges(before, after)) changed(before.rideHeight, after.rideHeight) { "${inches(it)} in" }?.let { add("Ride height $it") } changed(before.pinion, after.pinion) { "${formatDegrees(it)}°" }?.let { add("Pinion $it") } changed(before.brakeFront, after.brakeFront) { "${onePlace(it)}%" }?.let { add("Front brake $it") } } +private fun scaleChanges(before: SetupSheet, after: SetupSheet): List { + val old = scaleReading(before.weightLf, before.weightRf, before.weightLr, before.weightRr) + val next = scaleReading(after.weightLf, after.weightRf, after.weightLr, after.weightRr) + if (old == null || next == null || old == next) return emptyList() + return buildList { + if (old.total != next.total) add("Total ${old.total} lb → ${next.total} lb") + if (old.left != next.left || onePlace(old.leftPercent) != onePlace(next.leftPercent)) { + add("Left ${pair(old.left, old.leftPercent)} → ${pair(next.left, next.leftPercent)}") + } + if (old.front != next.front || onePlace(old.frontPercent) != onePlace(next.frontPercent)) { + add("Front ${pair(old.front, old.frontPercent)} → ${pair(next.front, next.frontPercent)}") + } + if (old.rear != next.rear || onePlace(old.rearPercent) != onePlace(next.rearPercent)) { + add("Rear ${pair(old.rear, old.rearPercent)} → ${pair(next.rear, next.rearPercent)}") + } + if (old.cross != next.cross || onePlace(old.crossPercent) != onePlace(next.crossPercent)) { + add("Cross ${pair(old.cross, old.crossPercent)} → ${pair(next.cross, next.crossPercent)}") + } + if (old.rearBite != next.rearBite) add("Rear bite ${old.rearBite} → ${next.rearBite}") + } +} + private fun percent(value: Double): String = String.format(Locale.US, "%.1f%%", value) private fun onePlace(value: Double): String = String.format(Locale.US, "%.1f", value).trimEnd('0').trimEnd('.') -private fun inches(value: Double): String = - String.format(Locale.US, "%.3f", value).trimEnd('0').trimEnd('.') +private fun inches(value: Double): String = formatInches(value) private fun changed( before: Double?, diff --git a/app/src/main/java/com/racingapp/pitlog/ui/Fields.kt b/app/src/main/java/com/racingapp/pitlog/ui/Fields.kt index 4ea67f4..ff8129d 100644 --- a/app/src/main/java/com/racingapp/pitlog/ui/Fields.kt +++ b/app/src/main/java/com/racingapp/pitlog/ui/Fields.kt @@ -94,14 +94,22 @@ fun NumberField( onValue: (String) -> Unit, modifier: Modifier = Modifier, decimal: Boolean = true, + inches: Boolean = false, + placeholder: String = "", ) { OutlinedTextField( value = value, onValueChange = onValue, label = { Text(label) }, + placeholder = if (placeholder.isEmpty()) null else ({ Text(placeholder) }), singleLine = true, keyboardOptions = KeyboardOptions( - keyboardType = if (decimal) KeyboardType.Decimal else KeyboardType.Number, + capitalization = KeyboardCapitalization.None, + keyboardType = when { + inches -> KeyboardType.Uri + decimal -> KeyboardType.Decimal + else -> KeyboardType.Number + }, ), textStyle = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), modifier = modifier, diff --git a/app/src/main/java/com/racingapp/pitlog/ui/night/NightDetailScreen.kt b/app/src/main/java/com/racingapp/pitlog/ui/night/NightDetailScreen.kt index 578e15f..15cdc91 100644 --- a/app/src/main/java/com/racingapp/pitlog/ui/night/NightDetailScreen.kt +++ b/app/src/main/java/com/racingapp/pitlog/ui/night/NightDetailScreen.kt @@ -49,6 +49,7 @@ import com.racingapp.pitlog.model.exportFileName import com.racingapp.pitlog.model.chassisSetup import com.racingapp.pitlog.model.chassisTips import com.racingapp.pitlog.model.formatDegrees +import com.racingapp.pitlog.model.formatInches import com.racingapp.pitlog.model.formatPsi import com.racingapp.pitlog.model.formatStagger import com.racingapp.pitlog.model.handlingText @@ -237,8 +238,8 @@ private fun SetupBlock(night: RaceNight, latest: RunLog?, onSetup: () -> Unit) { SetupLine( "Blocks", listOf( - night.blockLr?.let { "LR ${String.format(java.util.Locale.US, "%.1f", it)} in" }, - night.blockRr?.let { "RR ${String.format(java.util.Locale.US, "%.1f", it)} in" }, + night.blockLr?.let { "LR ${formatInches(it)} in" }, + night.blockRr?.let { "RR ${formatInches(it)} in" }, ).filterNotNull().joinToString(" ").ifBlank { null }, ) SetupLine( diff --git a/app/src/main/java/com/racingapp/pitlog/ui/night/SetupEditorScreen.kt b/app/src/main/java/com/racingapp/pitlog/ui/night/SetupEditorScreen.kt index c1aec30..0b1c834 100644 --- a/app/src/main/java/com/racingapp/pitlog/ui/night/SetupEditorScreen.kt +++ b/app/src/main/java/com/racingapp/pitlog/ui/night/SetupEditorScreen.kt @@ -31,9 +31,12 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.racingapp.pitlog.data.RaceNight import com.racingapp.pitlog.data.Repository +import com.racingapp.pitlog.model.formatInches import com.racingapp.pitlog.model.parseDecimal +import com.racingapp.pitlog.model.parseInches import com.racingapp.pitlog.model.parseIntField import com.racingapp.pitlog.model.sanitizeDecimal +import com.racingapp.pitlog.model.sanitizeInches import com.racingapp.pitlog.model.sanitizeInt import com.racingapp.pitlog.model.sanitizeSignedDecimal import com.racingapp.pitlog.ui.FieldRow @@ -71,8 +74,8 @@ private data class SetupForm( casterLf = parseDecimal(casterLf), casterRf = parseDecimal(casterRf), swayBarHole = parseIntField(swayBarHole), - blockLr = parseDecimal(blockLr), - blockRr = parseDecimal(blockRr), + blockLr = parseInches(blockLr), + blockRr = parseInches(blockRr), ) companion object { @@ -87,8 +90,8 @@ private data class SetupForm( casterLf = signedField(night.casterLf), casterRf = signedField(night.casterRf), swayBarHole = night.swayBarHole?.toString().orEmpty(), - blockLr = night.blockLr?.let { String.format(Locale.US, "%.1f", it) }.orEmpty(), - blockRr = night.blockRr?.let { String.format(Locale.US, "%.1f", it) }.orEmpty(), + blockLr = formatInches(night.blockLr), + blockRr = formatInches(night.blockRr), ) } } @@ -254,16 +257,20 @@ fun SetupEditorScreen(nightId: Long, onBack: () -> Unit) { label = "LR", value = form.blockLr, onValue = { value -> - viewModel.update { it.copy(blockLr = sanitizeDecimal(value, 1, 1)) } + viewModel.update { it.copy(blockLr = sanitizeInches(value)) } }, + inches = true, + placeholder = "1/2", modifier = Modifier.weight(1f), ) NumberField( label = "RR", value = form.blockRr, onValue = { value -> - viewModel.update { it.copy(blockRr = sanitizeDecimal(value, 1, 1)) } + viewModel.update { it.copy(blockRr = sanitizeInches(value)) } }, + inches = true, + placeholder = "1/2", modifier = Modifier.weight(1f), ) } diff --git a/app/src/main/java/com/racingapp/pitlog/ui/run/RunEditorScreen.kt b/app/src/main/java/com/racingapp/pitlog/ui/run/RunEditorScreen.kt index d00910b..f49e384 100644 --- a/app/src/main/java/com/racingapp/pitlog/ui/run/RunEditorScreen.kt +++ b/app/src/main/java/com/racingapp/pitlog/ui/run/RunEditorScreen.kt @@ -52,6 +52,7 @@ import com.racingapp.pitlog.model.Tread import com.racingapp.pitlog.model.formatPsi import com.racingapp.pitlog.model.hasColdPressure import com.racingapp.pitlog.model.sanitizeDecimal +import com.racingapp.pitlog.model.sanitizeInches import com.racingapp.pitlog.model.sanitizeInt import com.racingapp.pitlog.model.suspiciousReadings import com.racingapp.pitlog.model.toSheet @@ -261,8 +262,10 @@ fun RunEditorScreen( label = "Stagger", value = form.stagger, onValue = { value -> - viewModel.update { it.copy(stagger = sanitizeDecimal(value, 2, 2)) } + viewModel.update { it.copy(stagger = sanitizeInches(value)) } }, + inches = true, + placeholder = "1/8", modifier = Modifier.weight(1f), ) } diff --git a/app/src/main/java/com/racingapp/pitlog/ui/setup/CarSetupEditorScreen.kt b/app/src/main/java/com/racingapp/pitlog/ui/setup/CarSetupEditorScreen.kt index eae6c8c..8bc9d64 100644 --- a/app/src/main/java/com/racingapp/pitlog/ui/setup/CarSetupEditorScreen.kt +++ b/app/src/main/java/com/racingapp/pitlog/ui/setup/CarSetupEditorScreen.kt @@ -32,13 +32,16 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.racingapp.pitlog.data.Repository import com.racingapp.pitlog.data.SetupSheet +import com.racingapp.pitlog.model.formatInches import com.racingapp.pitlog.model.parseDecimal +import com.racingapp.pitlog.model.parseInches import com.racingapp.pitlog.model.parseIntField import com.racingapp.pitlog.model.sanitizeDecimal +import com.racingapp.pitlog.model.sanitizeInches import com.racingapp.pitlog.model.sanitizeInt import com.racingapp.pitlog.model.sanitizeSignedDecimal +import com.racingapp.pitlog.model.scaleLines import com.racingapp.pitlog.model.setupDiffs -import com.racingapp.pitlog.model.weightSplit import com.racingapp.pitlog.ui.FieldRow import com.racingapp.pitlog.ui.NumberField import com.racingapp.pitlog.ui.PitActionButton @@ -88,15 +91,15 @@ private data class CarSetupForm( casterLf = parseDecimal(casterLf), casterRf = parseDecimal(casterRf), swayBarHole = parseIntField(swayBarHole), - blockLr = parseDecimal(blockLr), - blockRr = parseDecimal(blockRr), - toeFront = parseDecimal(toeFront), - toeRear = parseDecimal(toeRear), + blockLr = parseInches(blockLr), + blockRr = parseInches(blockRr), + toeFront = parseInches(toeFront), + toeRear = parseInches(toeRear), weightLf = parseIntField(weightLf), weightRf = parseIntField(weightRf), weightLr = parseIntField(weightLr), weightRr = parseIntField(weightRr), - rideHeight = parseDecimal(rideHeight), + rideHeight = parseInches(rideHeight), pinion = parseDecimal(pinion), brakeFront = parseDecimal(brakeFront), ) @@ -113,15 +116,15 @@ private data class CarSetupForm( casterLf = decimal(sheet.casterLf, 1), casterRf = decimal(sheet.casterRf, 1), swayBarHole = sheet.swayBarHole?.toString().orEmpty(), - blockLr = decimal(sheet.blockLr, 1), - blockRr = decimal(sheet.blockRr, 1), - toeFront = decimal(sheet.toeFront, 3), - toeRear = decimal(sheet.toeRear, 3), + blockLr = formatInches(sheet.blockLr), + blockRr = formatInches(sheet.blockRr), + toeFront = formatInches(sheet.toeFront), + toeRear = formatInches(sheet.toeRear), weightLf = sheet.weightLf?.toString().orEmpty(), weightRf = sheet.weightRf?.toString().orEmpty(), weightLr = sheet.weightLr?.toString().orEmpty(), weightRr = sheet.weightRr?.toString().orEmpty(), - rideHeight = decimal(sheet.rideHeight, 1), + rideHeight = formatInches(sheet.rideHeight), pinion = decimal(sheet.pinion, 1), brakeFront = decimal(sheet.brakeFront, 1), note = if (keepNote) sheet.note else "", @@ -320,13 +323,17 @@ fun CarSetupEditorScreen( NumberField( label = "LR", value = form.blockLr, - onValue = { value -> viewModel.update { it.copy(blockLr = sanitizeDecimal(value, 1, 1)) } }, + onValue = { value -> viewModel.update { it.copy(blockLr = sanitizeInches(value)) } }, + inches = true, + placeholder = "1/2", modifier = Modifier.weight(1f), ) NumberField( label = "RR", value = form.blockRr, - onValue = { value -> viewModel.update { it.copy(blockRr = sanitizeDecimal(value, 1, 1)) } }, + onValue = { value -> viewModel.update { it.copy(blockRr = sanitizeInches(value)) } }, + inches = true, + placeholder = "1/2", modifier = Modifier.weight(1f), ) } @@ -354,12 +361,12 @@ fun CarSetupEditorScreen( color = MaterialTheme.colorScheme.onSurfaceVariant, ) FieldRow { - SignedField("Front", form.toeFront, { value -> viewModel.update { it.copy(toeFront = value) } }, Modifier.weight(1f), 1, 3) - SignedField("Rear", form.toeRear, { value -> viewModel.update { it.copy(toeRear = value) } }, Modifier.weight(1f), 1, 3) + SignedField("Front", form.toeFront, { value -> viewModel.update { it.copy(toeFront = value) } }, Modifier.weight(1f), inches = true) + SignedField("Rear", form.toeRear, { value -> viewModel.update { it.copy(toeRear = value) } }, Modifier.weight(1f), inches = true) } SectionLabel("Corner weights, lb") Text( - "Pounds on the pads. Left, rear, and cross are figured from these four.", + "Pounds on the pads. Left, front, rear, and cross come out in pounds and percent. Rear bite is left rear minus right rear.", color = MaterialTheme.colorScheme.onSurfaceVariant, ) FieldRow { @@ -394,13 +401,13 @@ fun CarSetupEditorScreen( modifier = Modifier.weight(1f), ) } - weightSplit( + scaleLines( form.weightLf.toIntOrNull(), form.weightRf.toIntOrNull(), form.weightLr.toIntOrNull(), form.weightRr.toIntOrNull(), - )?.let { split -> - Text(split, modifier = Modifier.padding(top = 8.dp)) + ).forEach { line -> + Text(line, modifier = Modifier.padding(top = 8.dp)) } SectionLabel("Ride height") Text( @@ -410,7 +417,9 @@ fun CarSetupEditorScreen( NumberField( label = "Inches", value = form.rideHeight, - onValue = { value -> viewModel.update { it.copy(rideHeight = sanitizeDecimal(value, 2, 1)) } }, + onValue = { value -> viewModel.update { it.copy(rideHeight = sanitizeInches(value)) } }, + inches = true, + placeholder = "5", modifier = Modifier.fillMaxWidth(), ) SectionLabel("Pinion") @@ -457,6 +466,7 @@ private fun SignedField( value: String, onValue: (String) -> Unit, modifier: Modifier = Modifier, + inches: Boolean = false, wholeDigits: Int = 2, fractionDigits: Int = 1, ) { @@ -464,7 +474,14 @@ private fun SignedField( NumberField( label = label, value = value, - onValue = { onValue(sanitizeSignedDecimal(it, wholeDigits, fractionDigits)) }, + onValue = { + onValue( + if (inches) sanitizeInches(it, signed = true) + else sanitizeSignedDecimal(it, wholeDigits, fractionDigits), + ) + }, + inches = inches, + placeholder = if (inches) "1/16" else "", modifier = Modifier.fillMaxWidth(), ) TextButton( diff --git a/app/src/test/java/com/racingapp/pitlog/model/InchesTest.kt b/app/src/test/java/com/racingapp/pitlog/model/InchesTest.kt new file mode 100644 index 0000000..d5273ce --- /dev/null +++ b/app/src/test/java/com/racingapp/pitlog/model/InchesTest.kt @@ -0,0 +1,31 @@ +package com.racingapp.pitlog.model + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class InchesTest { + @Test + fun aFractionParsesAndComesBackAsAFraction() { + assertEquals(0.0625, parseInches("1/16")!!, 0.00001) + assertEquals(0.125, parseInches("1/8")!!, 0.00001) + assertEquals(2.5, parseInches("2 1/2")!!, 0.00001) + assertEquals(-0.0625, parseInches("-1/16")!!, 0.00001) + assertEquals(1.125, parseInches("1.125")!!, 0.00001) + assertEquals("1/16", formatInches(parseInches("1/16"))) + assertEquals("1/8", formatInches(parseInches("1/8"))) + assertEquals("2 1/2", formatInches(parseInches("2 1/2"))) + assertEquals("-1/16", formatInches(parseInches("-1/16"))) + assertEquals("1 1/8", formatInches(1.125)) + } + + @Test + fun typingKeepsTheSlashUntilTheFractionIsFinished() { + assertEquals("1/16", sanitizeInches("1/16")) + assertEquals("1/", sanitizeInches("1/")) + assertEquals("2 1/2", sanitizeInches("2 1/2")) + assertEquals("-1/8", sanitizeInches("-1/8", signed = true)) + assertNull(parseInches("1/")) + assertNull(parseInches("1/0")) + } +} diff --git a/app/src/test/java/com/racingapp/pitlog/model/SetupLogTest.kt b/app/src/test/java/com/racingapp/pitlog/model/SetupLogTest.kt index e825be2..e7bd197 100644 --- a/app/src/test/java/com/racingapp/pitlog/model/SetupLogTest.kt +++ b/app/src/test/java/com/racingapp/pitlog/model/SetupLogTest.kt @@ -23,7 +23,7 @@ class SetupLogTest { listOf( "RF spring 1000 lb → 1050 lb", "Sway bar Setting 4 → Setting 3", - "Front toe 0.125 in → 0.063 in", + "Front toe 1/8 in → 1/16 in", ), setupDiffs(original, later), ) @@ -56,7 +56,12 @@ class SetupLogTest { assertEquals( listOf( "Weights LF 800 RF 760 LR 740 RR 700 lb", - "From the weights Left 51.3% Rear 48% Cross 50%", + "Total 3000 lb", + "Left 1540 lb, 51.3%", + "Front 1560 lb, 52%", + "Rear 1440 lb, 48%", + "Cross 1500 lb, 50%", + "Rear bite 40", "Ride height 5 in", "Pinion -2.0°", "Front brake 62%", @@ -66,6 +71,11 @@ class SetupLogTest { assertEquals( listOf( "LF weight 800 lb → 820 lb", + "Total 3000 lb → 3020 lb", + "Left 1540 lb, 51.3% → 1560 lb, 51.7%", + "Front 1560 lb, 52% → 1580 lb, 52.3%", + "Rear 1440 lb, 48% → 1440 lb, 47.7%", + "Cross 1500 lb, 50% → 1500 lb, 49.7%", "Ride height 5 in → 5.2 in", "Pinion -2.0° → -1.5°", "Front brake 62% → 65%",