Publish 0.023 alpha so inch fields take fractions and the scales read out on the car setup.
This commit is contained in:
1 parent
b1f0367b57
commit
6f1fdeeb2f
11 files changed
+291
-49
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
|
||||
@@ -28,22 +28,61 @@ fun setupLines(sheet: SetupSheet): List<String> = 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<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
|
||||
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<String> = 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<String> = 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<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 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?,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
"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%",
|
||||
|
||||
Reference in new issue
Block a user