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"
|
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%",
|
||||||
|
|||||||
Reference in new issue
Block a user