Coverage Summary for Class: NumpadKey (com.opty.numpad)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| NumpadKey$Backspace |
100%
(1/1)
|
|
100%
(1/1)
|
100%
(2/2)
|
| NumpadKey$DecimalSeparator |
100%
(1/1)
|
|
100%
(1/1)
|
100%
(2/2)
|
| NumpadKey$Digit |
100%
(1/1)
|
50%
(3/6)
|
100%
(4/4)
|
65.6%
(21/32)
|
| Total |
100%
(3/3)
|
50%
(3/6)
|
100%
(6/6)
|
69.4%
(25/36)
|
/*
* Copyright 2024 Opty All rights reserved.
*
* This software and associated documentation files (the "Software") are protected by copyright law and
* international treaties. This software has been licensed to users under the terms of the End User
* License Agreement (EULA) and Terms of Service (ToS) available within the application.
*
* Unauthorized copying, reproduction, modification, distribution, display, performance, sublicense, sale,
* access or use of this Software, or any part thereof, is strictly prohibited and will be prosecuted
* under applicable laws.
*
* The Software contains valuable trade secrets and proprietary information of Opty.
* No part of the Software may be reproduced, distributed, or transmitted in any form or by any means,
* including photocopying, recording, or other electronic or mechanical methods without the prior written
* permission of Opty.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* For licensing inquiries, please contact: support@optymacros.com
*/
package com.opty.numpad
import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import kotlin.math.max
import kotlin.math.min
@Immutable
data class NumpadInputSpec(
val maxValue: Double,
val decimalSeparator: Char,
val maxFractionDigits: Int,
val maxLength: Int =
maxValue.toLong().toString().length +
if (maxFractionDigits > 0) maxFractionDigits + 1 else 0,
) {
init {
require(maxValue >= 0.0)
require(maxFractionDigits >= 0)
require(maxLength > 0)
}
val allowsDecimal: Boolean
get() = maxFractionDigits > 0
}
internal sealed interface NumpadKey {
data class Digit(val value: Int) : NumpadKey {
init {
require(value in 0..9)
}
}
data object DecimalSeparator : NumpadKey
data object Backspace : NumpadKey
}
internal fun TextFieldValue.applyNumpadKey(
key: NumpadKey,
spec: NumpadInputSpec,
): TextFieldValue {
val selectionStart = min(selection.start, selection.end).coerceIn(0, text.length)
val selectionEnd = max(selection.start, selection.end).coerceIn(0, text.length)
val replacement =
when (key) {
is NumpadKey.Digit -> key.value.toString()
NumpadKey.DecimalSeparator -> {
if (!spec.allowsDecimal) return this
spec.decimalSeparator.toString()
}
NumpadKey.Backspace -> ""
}
val replacementRange =
when {
key == NumpadKey.Backspace && selectionStart != selectionEnd -> selectionStart until selectionEnd
key == NumpadKey.Backspace && selectionStart > 0 -> (selectionStart - 1) until selectionStart
key == NumpadKey.Backspace -> return this
key is NumpadKey.Digit && text == "0" && selectionStart == selectionEnd -> 0 until text.length
else -> selectionStart until selectionEnd
}
val rawCandidate = text.replaceRange(replacementRange, replacement)
val candidate =
when {
rawCandidate.isEmpty() -> "0"
rawCandidate == spec.decimalSeparator.toString() -> "0$rawCandidate"
else -> rawCandidate
}
val cursor =
if (rawCandidate.isEmpty()) {
1
} else {
replacementRange.first + replacement.length + if (candidate.length > rawCandidate.length) 1 else 0
}.coerceIn(0, candidate.length)
if (!candidate.isValidFor(spec)) return this
return copy(text = candidate, selection = TextRange(cursor))
}
private fun String.isValidFor(spec: NumpadInputSpec): Boolean {
if (isEmpty()) return false
if (length > spec.maxLength) return false
if (any { !it.isDigit() && it != spec.decimalSeparator }) return false
if (count { it == spec.decimalSeparator } > 1) return false
val separatorIndex = indexOf(spec.decimalSeparator)
if (!spec.allowsDecimal && separatorIndex >= 0) return false
if (separatorIndex >= 0 && length - separatorIndex - 1 > spec.maxFractionDigits) return false
val parsedValue = replace(spec.decimalSeparator, '.').toDoubleOrNull() ?: return false
return parsedValue <= spec.maxValue
}