Publish 0.023 alpha so inch fields take fractions and the scales read out on the car setup.

This commit is contained in:
RallyBlock committed 2026-10-22 23:57:27 -07:00
1 parent b1f0367b57
commit 6f1fdeeb2f
11 files changed
+291 -49

No files matched your search

+5 -2
View File
@@ -13,8 +13,11 @@ android {
applicationId = "com.racingapp.pitlog" applicationId = "com.racingapp.pitlog"
minSdk = 26 minSdk = 26
targetSdk = 36 targetSdk = 36
versionCode = 3 // Three digits. versionCode is that number with the dot removed.
versionName = "0.003" // 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 { buildTypes {
@@ -1,6 +1,8 @@
package com.racingapp.pitlog.model package com.racingapp.pitlog.model
import java.util.Locale import java.util.Locale
import kotlin.math.abs
import kotlin.math.roundToInt
fun sanitizeSignedDecimal(raw: String, wholeDigits: Int, fractionDigits: Int): String { fun sanitizeSignedDecimal(raw: String, wholeDigits: Int, fractionDigits: Int): String {
val negative = raw.trimStart().startsWith('-') val negative = raw.trimStart().startsWith('-')
@@ -31,6 +33,72 @@ fun parseDecimal(raw: String): Double? {
return trimmed.toDoubleOrNull() 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 parseIntField(raw: String): Int? = raw.trim().toIntOrNull()
fun formatPsi(value: Double?): String = fun formatPsi(value: Double?): String =
@@ -52,15 +120,48 @@ fun formatDegrees(value: Double?): String =
fun formatStagger(value: Double?): String { fun formatStagger(value: Double?): String {
if (value == null) return "—" if (value == null) return "—"
return String.format(Locale.US, "%.2f", value).trimEnd('0').trimEnd('.') return formatInches(value)
} }
fun psiField(value: Double?): String = fun psiField(value: Double?): String =
if (value == null) "" else String.format(Locale.US, "%.1f", value) if (value == null) "" else String.format(Locale.US, "%.1f", value)
fun staggerField(value: Double?): String { fun staggerField(value: Double?): String = formatInches(value)
if (value == null) return ""
return String.format(Locale.US, "%.2f", value).trimEnd('0').trimEnd('.') 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() fun intField(value: Int?): String = value?.toString().orEmpty()
@@ -123,7 +123,7 @@ data class RunForm(
if (sheet.cold.any() || sheet.hot.any()) return true if (sheet.cold.any() || sheet.hot.any()) return true
if (sheet.heat.values.any { it.any() }) return true if (sheet.heat.values.any { it.any() }) return true
if (parseIntField(laps) != null || parseIntField(finish) != null) 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 if (tireSet.isNotBlank() || notes.isNotBlank()) return true
return handling != null || handlingWhere != null return handling != null || handlingWhere != null
} }
@@ -137,7 +137,7 @@ data class RunForm(
tireSet = tireSet.trim(), tireSet = tireSet.trim(),
laps = parseIntField(laps), laps = parseIntField(laps),
finish = parseIntField(finish), finish = parseIntField(finish),
staggerInches = parseDecimal(stagger), staggerInches = parseInches(stagger),
coldLf = sheet.cold.lf, coldLf = sheet.cold.lf,
coldRf = sheet.cold.rf, coldRf = sheet.cold.rf,
coldLr = sheet.cold.lr, coldLr = sheet.cold.lr,
@@ -28,22 +28,61 @@ fun setupLines(sheet: SetupSheet): List<String> = buildList {
"RR" to sheet.weightRr, "RR" to sheet.weightRr,
).mapNotNull { (corner, pounds) -> pounds?.let { "$corner $it" } } ).mapNotNull { (corner, pounds) -> pounds?.let { "$corner $it" } }
if (weights.isNotEmpty()) add("Weights ${weights.joinToString(" ")} lb") 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.rideHeight?.let { add("Ride height ${inches(it)} in") }
sheet.pinion?.let { add("Pinion ${formatDegrees(it)}°") } sheet.pinion?.let { add("Pinion ${formatDegrees(it)}°") }
sheet.brakeFront?.let { add("Front brake ${onePlace(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<String> {
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 if (lf == null || rf == null || lr == null || rr == null) return null
val total = lf + rf + lr + rr val total = lf + rf + lr + rr
if (total <= 0) return null if (total <= 0) return null
val left = (lf + lr) * 100.0 / total val left = lf + lr
val rear = (lr + rr) * 100.0 / total val front = lf + rf
val cross = (rf + lr) * 100.0 / total val rear = lr + rr
return "Left ${onePlace(left)}% Rear ${onePlace(rear)}% Cross ${onePlace(cross)}%" 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<String> = buildList { fun setupDiffs(before: SetupSheet, after: SetupSheet): List<String> = buildList {
changed(before.crossPercent, after.crossPercent, ::percent)?.let { add("Cross $it") } changed(before.crossPercent, after.crossPercent, ::percent)?.let { add("Cross $it") }
changedInt(before.springLf, after.springLf, " lb")?.let { add("LF spring $it") } changedInt(before.springLf, after.springLf, " lb")?.let { add("LF spring $it") }
@@ -63,18 +102,40 @@ fun setupDiffs(before: SetupSheet, after: SetupSheet): List<String> = buildList
changedInt(before.weightRf, after.weightRf, " lb")?.let { add("RF weight $it") } changedInt(before.weightRf, after.weightRf, " lb")?.let { add("RF weight $it") }
changedInt(before.weightLr, after.weightLr, " lb")?.let { add("LR weight $it") } changedInt(before.weightLr, after.weightLr, " lb")?.let { add("LR weight $it") }
changedInt(before.weightRr, after.weightRr, " lb")?.let { add("RR 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.rideHeight, after.rideHeight) { "${inches(it)} in" }?.let { add("Ride height $it") }
changed(before.pinion, after.pinion) { "${formatDegrees(it)}°" }?.let { add("Pinion $it") } changed(before.pinion, after.pinion) { "${formatDegrees(it)}°" }?.let { add("Pinion $it") }
changed(before.brakeFront, after.brakeFront) { "${onePlace(it)}%" }?.let { add("Front brake $it") } changed(before.brakeFront, after.brakeFront) { "${onePlace(it)}%" }?.let { add("Front brake $it") }
} }
private fun scaleChanges(before: SetupSheet, after: SetupSheet): List<String> {
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 percent(value: Double): String = String.format(Locale.US, "%.1f%%", value)
private fun onePlace(value: Double): String = private fun onePlace(value: Double): String =
String.format(Locale.US, "%.1f", value).trimEnd('0').trimEnd('.') String.format(Locale.US, "%.1f", value).trimEnd('0').trimEnd('.')
private fun inches(value: Double): String = private fun inches(value: Double): String = formatInches(value)
String.format(Locale.US, "%.3f", value).trimEnd('0').trimEnd('.')
private fun changed( private fun changed(
before: Double?, before: Double?,
@@ -94,14 +94,22 @@ fun NumberField(
onValue: (String) -> Unit, onValue: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
decimal: Boolean = true, decimal: Boolean = true,
inches: Boolean = false,
placeholder: String = "",
) { ) {
OutlinedTextField( OutlinedTextField(
value = value, value = value,
onValueChange = onValue, onValueChange = onValue,
label = { Text(label) }, label = { Text(label) },
placeholder = if (placeholder.isEmpty()) null else ({ Text(placeholder) }),
singleLine = true, singleLine = true,
keyboardOptions = KeyboardOptions( 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), textStyle = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
modifier = modifier, modifier = modifier,
@@ -49,6 +49,7 @@ import com.racingapp.pitlog.model.exportFileName
import com.racingapp.pitlog.model.chassisSetup import com.racingapp.pitlog.model.chassisSetup
import com.racingapp.pitlog.model.chassisTips import com.racingapp.pitlog.model.chassisTips
import com.racingapp.pitlog.model.formatDegrees import com.racingapp.pitlog.model.formatDegrees
import com.racingapp.pitlog.model.formatInches
import com.racingapp.pitlog.model.formatPsi import com.racingapp.pitlog.model.formatPsi
import com.racingapp.pitlog.model.formatStagger import com.racingapp.pitlog.model.formatStagger
import com.racingapp.pitlog.model.handlingText import com.racingapp.pitlog.model.handlingText
@@ -237,8 +238,8 @@ private fun SetupBlock(night: RaceNight, latest: RunLog?, onSetup: () -> Unit) {
SetupLine( SetupLine(
"Blocks", "Blocks",
listOf( listOf(
night.blockLr?.let { "LR ${String.format(java.util.Locale.US, "%.1f", it)} in" }, night.blockLr?.let { "LR ${formatInches(it)} in" },
night.blockRr?.let { "RR ${String.format(java.util.Locale.US, "%.1f", it)} in" }, night.blockRr?.let { "RR ${formatInches(it)} in" },
).filterNotNull().joinToString(" ").ifBlank { null }, ).filterNotNull().joinToString(" ").ifBlank { null },
) )
SetupLine( SetupLine(
@@ -31,9 +31,12 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.racingapp.pitlog.data.RaceNight import com.racingapp.pitlog.data.RaceNight
import com.racingapp.pitlog.data.Repository import com.racingapp.pitlog.data.Repository
import com.racingapp.pitlog.model.formatInches
import com.racingapp.pitlog.model.parseDecimal import com.racingapp.pitlog.model.parseDecimal
import com.racingapp.pitlog.model.parseInches
import com.racingapp.pitlog.model.parseIntField import com.racingapp.pitlog.model.parseIntField
import com.racingapp.pitlog.model.sanitizeDecimal import com.racingapp.pitlog.model.sanitizeDecimal
import com.racingapp.pitlog.model.sanitizeInches
import com.racingapp.pitlog.model.sanitizeInt import com.racingapp.pitlog.model.sanitizeInt
import com.racingapp.pitlog.model.sanitizeSignedDecimal import com.racingapp.pitlog.model.sanitizeSignedDecimal
import com.racingapp.pitlog.ui.FieldRow import com.racingapp.pitlog.ui.FieldRow
@@ -71,8 +74,8 @@ private data class SetupForm(
casterLf = parseDecimal(casterLf), casterLf = parseDecimal(casterLf),
casterRf = parseDecimal(casterRf), casterRf = parseDecimal(casterRf),
swayBarHole = parseIntField(swayBarHole), swayBarHole = parseIntField(swayBarHole),
blockLr = parseDecimal(blockLr), blockLr = parseInches(blockLr),
blockRr = parseDecimal(blockRr), blockRr = parseInches(blockRr),
) )
companion object { companion object {
@@ -87,8 +90,8 @@ private data class SetupForm(
casterLf = signedField(night.casterLf), casterLf = signedField(night.casterLf),
casterRf = signedField(night.casterRf), casterRf = signedField(night.casterRf),
swayBarHole = night.swayBarHole?.toString().orEmpty(), swayBarHole = night.swayBarHole?.toString().orEmpty(),
blockLr = night.blockLr?.let { String.format(Locale.US, "%.1f", it) }.orEmpty(), blockLr = formatInches(night.blockLr),
blockRr = night.blockRr?.let { String.format(Locale.US, "%.1f", it) }.orEmpty(), blockRr = formatInches(night.blockRr),
) )
} }
} }
@@ -254,16 +257,20 @@ fun SetupEditorScreen(nightId: Long, onBack: () -> Unit) {
label = "LR", label = "LR",
value = form.blockLr, value = form.blockLr,
onValue = { value -> 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), modifier = Modifier.weight(1f),
) )
NumberField( NumberField(
label = "RR", label = "RR",
value = form.blockRr, value = form.blockRr,
onValue = { value -> 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), modifier = Modifier.weight(1f),
) )
} }
@@ -52,6 +52,7 @@ import com.racingapp.pitlog.model.Tread
import com.racingapp.pitlog.model.formatPsi import com.racingapp.pitlog.model.formatPsi
import com.racingapp.pitlog.model.hasColdPressure import com.racingapp.pitlog.model.hasColdPressure
import com.racingapp.pitlog.model.sanitizeDecimal import com.racingapp.pitlog.model.sanitizeDecimal
import com.racingapp.pitlog.model.sanitizeInches
import com.racingapp.pitlog.model.sanitizeInt import com.racingapp.pitlog.model.sanitizeInt
import com.racingapp.pitlog.model.suspiciousReadings import com.racingapp.pitlog.model.suspiciousReadings
import com.racingapp.pitlog.model.toSheet import com.racingapp.pitlog.model.toSheet
@@ -261,8 +262,10 @@ fun RunEditorScreen(
label = "Stagger", label = "Stagger",
value = form.stagger, value = form.stagger,
onValue = { value -> 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), modifier = Modifier.weight(1f),
) )
} }
@@ -32,13 +32,16 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.racingapp.pitlog.data.Repository import com.racingapp.pitlog.data.Repository
import com.racingapp.pitlog.data.SetupSheet import com.racingapp.pitlog.data.SetupSheet
import com.racingapp.pitlog.model.formatInches
import com.racingapp.pitlog.model.parseDecimal import com.racingapp.pitlog.model.parseDecimal
import com.racingapp.pitlog.model.parseInches
import com.racingapp.pitlog.model.parseIntField import com.racingapp.pitlog.model.parseIntField
import com.racingapp.pitlog.model.sanitizeDecimal import com.racingapp.pitlog.model.sanitizeDecimal
import com.racingapp.pitlog.model.sanitizeInches
import com.racingapp.pitlog.model.sanitizeInt import com.racingapp.pitlog.model.sanitizeInt
import com.racingapp.pitlog.model.sanitizeSignedDecimal import com.racingapp.pitlog.model.sanitizeSignedDecimal
import com.racingapp.pitlog.model.scaleLines
import com.racingapp.pitlog.model.setupDiffs import com.racingapp.pitlog.model.setupDiffs
import com.racingapp.pitlog.model.weightSplit
import com.racingapp.pitlog.ui.FieldRow import com.racingapp.pitlog.ui.FieldRow
import com.racingapp.pitlog.ui.NumberField import com.racingapp.pitlog.ui.NumberField
import com.racingapp.pitlog.ui.PitActionButton import com.racingapp.pitlog.ui.PitActionButton
@@ -88,15 +91,15 @@ private data class CarSetupForm(
casterLf = parseDecimal(casterLf), casterLf = parseDecimal(casterLf),
casterRf = parseDecimal(casterRf), casterRf = parseDecimal(casterRf),
swayBarHole = parseIntField(swayBarHole), swayBarHole = parseIntField(swayBarHole),
blockLr = parseDecimal(blockLr), blockLr = parseInches(blockLr),
blockRr = parseDecimal(blockRr), blockRr = parseInches(blockRr),
toeFront = parseDecimal(toeFront), toeFront = parseInches(toeFront),
toeRear = parseDecimal(toeRear), toeRear = parseInches(toeRear),
weightLf = parseIntField(weightLf), weightLf = parseIntField(weightLf),
weightRf = parseIntField(weightRf), weightRf = parseIntField(weightRf),
weightLr = parseIntField(weightLr), weightLr = parseIntField(weightLr),
weightRr = parseIntField(weightRr), weightRr = parseIntField(weightRr),
rideHeight = parseDecimal(rideHeight), rideHeight = parseInches(rideHeight),
pinion = parseDecimal(pinion), pinion = parseDecimal(pinion),
brakeFront = parseDecimal(brakeFront), brakeFront = parseDecimal(brakeFront),
) )
@@ -113,15 +116,15 @@ private data class CarSetupForm(
casterLf = decimal(sheet.casterLf, 1), casterLf = decimal(sheet.casterLf, 1),
casterRf = decimal(sheet.casterRf, 1), casterRf = decimal(sheet.casterRf, 1),
swayBarHole = sheet.swayBarHole?.toString().orEmpty(), swayBarHole = sheet.swayBarHole?.toString().orEmpty(),
blockLr = decimal(sheet.blockLr, 1), blockLr = formatInches(sheet.blockLr),
blockRr = decimal(sheet.blockRr, 1), blockRr = formatInches(sheet.blockRr),
toeFront = decimal(sheet.toeFront, 3), toeFront = formatInches(sheet.toeFront),
toeRear = decimal(sheet.toeRear, 3), toeRear = formatInches(sheet.toeRear),
weightLf = sheet.weightLf?.toString().orEmpty(), weightLf = sheet.weightLf?.toString().orEmpty(),
weightRf = sheet.weightRf?.toString().orEmpty(), weightRf = sheet.weightRf?.toString().orEmpty(),
weightLr = sheet.weightLr?.toString().orEmpty(), weightLr = sheet.weightLr?.toString().orEmpty(),
weightRr = sheet.weightRr?.toString().orEmpty(), weightRr = sheet.weightRr?.toString().orEmpty(),
rideHeight = decimal(sheet.rideHeight, 1), rideHeight = formatInches(sheet.rideHeight),
pinion = decimal(sheet.pinion, 1), pinion = decimal(sheet.pinion, 1),
brakeFront = decimal(sheet.brakeFront, 1), brakeFront = decimal(sheet.brakeFront, 1),
note = if (keepNote) sheet.note else "", note = if (keepNote) sheet.note else "",
@@ -320,13 +323,17 @@ fun CarSetupEditorScreen(
NumberField( NumberField(
label = "LR", label = "LR",
value = form.blockLr, 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), modifier = Modifier.weight(1f),
) )
NumberField( NumberField(
label = "RR", label = "RR",
value = form.blockRr, 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), modifier = Modifier.weight(1f),
) )
} }
@@ -354,12 +361,12 @@ fun CarSetupEditorScreen(
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
FieldRow { FieldRow {
SignedField("Front", form.toeFront, { value -> viewModel.update { it.copy(toeFront = 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), 1, 3) SignedField("Rear", form.toeRear, { value -> viewModel.update { it.copy(toeRear = value) } }, Modifier.weight(1f), inches = true)
} }
SectionLabel("Corner weights, lb") SectionLabel("Corner weights, lb")
Text( 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, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
FieldRow { FieldRow {
@@ -394,13 +401,13 @@ fun CarSetupEditorScreen(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
} }
weightSplit( scaleLines(
form.weightLf.toIntOrNull(), form.weightLf.toIntOrNull(),
form.weightRf.toIntOrNull(), form.weightRf.toIntOrNull(),
form.weightLr.toIntOrNull(), form.weightLr.toIntOrNull(),
form.weightRr.toIntOrNull(), form.weightRr.toIntOrNull(),
)?.let { split -> ).forEach { line ->
Text(split, modifier = Modifier.padding(top = 8.dp)) Text(line, modifier = Modifier.padding(top = 8.dp))
} }
SectionLabel("Ride height") SectionLabel("Ride height")
Text( Text(
@@ -410,7 +417,9 @@ fun CarSetupEditorScreen(
NumberField( NumberField(
label = "Inches", label = "Inches",
value = form.rideHeight, 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(), modifier = Modifier.fillMaxWidth(),
) )
SectionLabel("Pinion") SectionLabel("Pinion")
@@ -457,6 +466,7 @@ private fun SignedField(
value: String, value: String,
onValue: (String) -> Unit, onValue: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
inches: Boolean = false,
wholeDigits: Int = 2, wholeDigits: Int = 2,
fractionDigits: Int = 1, fractionDigits: Int = 1,
) { ) {
@@ -464,7 +474,14 @@ private fun SignedField(
NumberField( NumberField(
label = label, label = label,
value = value, 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(), modifier = Modifier.fillMaxWidth(),
) )
TextButton( TextButton(
@@ -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"))
}
}
@@ -23,7 +23,7 @@ class SetupLogTest {
listOf( listOf(
"RF spring 1000 lb → 1050 lb", "RF spring 1000 lb → 1050 lb",
"Sway bar Setting 4 → Setting 3", "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), setupDiffs(original, later),
) )
@@ -56,7 +56,12 @@ class SetupLogTest {
assertEquals( assertEquals(
listOf( listOf(
"Weights LF 800 RF 760 LR 740 RR 700 lb", "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", "Ride height 5 in",
"Pinion -2.0°", "Pinion -2.0°",
"Front brake 62%", "Front brake 62%",
@@ -66,6 +71,11 @@ class SetupLogTest {
assertEquals( assertEquals(
listOf( listOf(
"LF weight 800 lb → 820 lb", "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", "Ride height 5 in → 5.2 in",
"Pinion -2.0° → -1.5°", "Pinion -2.0° → -1.5°",
"Front brake 62% → 65%", "Front brake 62% → 65%",