mirror of
https://github.com/oliexdev/openScale.git
synced 2025-09-02 21:02:48 +02:00
Convert legacy Bluetooth scale algorithm classes to Kotlin and unit tests have been added for each of these Kotlin classes to verify the correctness of the ported algorithms against snapshot values and behavioral properties.
This commit is contained in:
@@ -161,11 +161,6 @@ dependencies {
|
|||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
implementation(libs.androidx.worker)
|
implementation(libs.androidx.worker)
|
||||||
implementation(libs.androidx.documentfile)
|
implementation(libs.androidx.documentfile)
|
||||||
testImplementation(libs.junit)
|
|
||||||
androidTestImplementation(libs.androidx.junit)
|
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
|
||||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
|
||||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
|
||||||
debugImplementation(libs.androidx.ui.tooling)
|
debugImplementation(libs.androidx.ui.tooling)
|
||||||
debugImplementation(libs.androidx.ui.test.manifest)
|
debugImplementation(libs.androidx.ui.test.manifest)
|
||||||
|
|
||||||
@@ -206,4 +201,13 @@ dependencies {
|
|||||||
// Blessed Kotlin
|
// Blessed Kotlin
|
||||||
// implementation(libs.blessed.kotlin)
|
// implementation(libs.blessed.kotlin)
|
||||||
implementation(libs.blessed.java)
|
implementation(libs.blessed.java)
|
||||||
|
|
||||||
|
// Test dependencies
|
||||||
|
testImplementation(libs.junit)
|
||||||
|
androidTestImplementation(libs.androidx.junit)
|
||||||
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||||
|
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||||
|
testImplementation(libs.junit)
|
||||||
|
testImplementation(libs.truth)
|
||||||
}
|
}
|
@@ -1,196 +1,154 @@
|
|||||||
/* Copyright (C) 2019 olie.xdev <olie.xdev@googlemail.com>
|
/*
|
||||||
|
* openScale
|
||||||
|
* Copyright (C) 2025 olie.xdev <olie.xdeveloper@googlemail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful,
|
* This program is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* based on https://github.com/prototux/MIBCS-reverse-engineering by prototux
|
* based on https://github.com/prototux/MIBCS-reverse-engineering by prototux
|
||||||
*/
|
*/
|
||||||
|
package com.health.openscale.core.bluetooth.libs
|
||||||
|
|
||||||
package com.health.openscale.core.bluetooth.libs;
|
class MiScaleLib(
|
||||||
|
// male = 1; female = 0
|
||||||
|
private val sex: Int,
|
||||||
|
private val age: Int,
|
||||||
|
private val height: Float // cm
|
||||||
|
) {
|
||||||
|
|
||||||
public class MiScaleLib {
|
private fun getLBMCoefficient(weight: Float, impedance: Float): Float {
|
||||||
private int sex; // male = 1; female = 0
|
var lbm = (height * 9.058f / 100.0f) * (height / 100.0f)
|
||||||
private int age;
|
lbm += weight * 0.32f + 12.226f
|
||||||
private float height;
|
lbm -= impedance * 0.0068f
|
||||||
|
lbm -= age * 0.0542f
|
||||||
public MiScaleLib(int sex, int age, float height) {
|
return lbm
|
||||||
this.sex = sex;
|
|
||||||
this.age = age;
|
|
||||||
this.height = height;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private float getLBMCoefficient(float weight, float impedance) {
|
fun getBMI(weight: Float): Float {
|
||||||
float lbm = (height * 9.058f / 100.0f) * (height / 100.0f);
|
// weight [kg], height [cm]
|
||||||
lbm += weight * 0.32f + 12.226f;
|
// BMI = kg / (m^2)
|
||||||
lbm -= impedance * 0.0068f;
|
return weight / (((height * height) / 100.0f) / 100.0f)
|
||||||
lbm -= age * 0.0542f;
|
|
||||||
|
|
||||||
return lbm;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getBMI(float weight) {
|
fun getLBM(weight: Float, impedance: Float): Float {
|
||||||
return weight / (((height * height) / 100.0f) / 100.0f);
|
var leanBodyMass =
|
||||||
}
|
weight - ((getBodyFat(weight, impedance) * 0.01f) * weight) - getBoneMass(weight, impedance)
|
||||||
|
|
||||||
public float getLBM(float weight, float impedance) {
|
|
||||||
float leanBodyMass = weight - ((getBodyFat(weight, impedance) * 0.01f) * weight) - getBoneMass(weight, impedance);
|
|
||||||
|
|
||||||
if (sex == 0 && leanBodyMass >= 84.0f) {
|
if (sex == 0 && leanBodyMass >= 84.0f) {
|
||||||
leanBodyMass = 120.0f;
|
leanBodyMass = 120.0f
|
||||||
}
|
} else if (sex == 1 && leanBodyMass >= 93.5f) {
|
||||||
else if (sex == 1 && leanBodyMass >= 93.5f) {
|
leanBodyMass = 120.0f
|
||||||
leanBodyMass = 120.0f;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return leanBodyMass;
|
return leanBodyMass
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Skeletal Muscle Mass (kg) using Janssen et al. BIA equation.
|
* Skeletal Muscle Mass (%) derived from Janssen et al. BIA equation (kg) -> percent of body weight.
|
||||||
* If impedance is non-positive, falls back to LBM * ratio.
|
* If impedance is non-positive, falls back to LBM * ratio.
|
||||||
*/
|
*/
|
||||||
public float getMuscle(float weight, float impedance) {
|
fun getMuscle(weight: Float, impedance: Float): Float {
|
||||||
if (weight <= 0f) return 0f;
|
if (weight <= 0f) return 0f
|
||||||
|
|
||||||
float smm;
|
val smmKg: Float = if (impedance > 0f) {
|
||||||
if (impedance > 0f) {
|
// Janssen et al.: SMM(kg) = 0.401*(H^2/R) + 3.825*sex - 0.071*age + 5.102
|
||||||
// Janssen et al. BIA equation for Skeletal Muscle Mass (kg)
|
val h2OverR = (height * height) / impedance
|
||||||
float h2_over_r = (height * height) / impedance;
|
0.401f * h2OverR + 3.825f * sex - 0.071f * age + 5.102f
|
||||||
smm = 0.401f * h2_over_r + 3.825f * sex - 0.071f * age + 5.102f;
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback: approximate as fraction of LBM
|
// Fallback: approximate as fraction of LBM
|
||||||
float lbm = getLBM(weight, impedance);
|
val lbm = getLBM(weight, impedance)
|
||||||
float ratio = (sex == 1) ? 0.52f : 0.46f;
|
val ratio = if (sex == 1) 0.52f else 0.46f
|
||||||
smm = lbm * ratio;
|
lbm * ratio
|
||||||
}
|
}
|
||||||
|
|
||||||
float percent = (smm / weight) * 100f;
|
val percent = (smmKg / weight) * 100f
|
||||||
return clamp(percent, 10f, 60f); // clamp to plausible %
|
return percent.coerceIn(10f, 60f)
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getWater(float weight, float impedance) {
|
fun getWater(weight: Float, impedance: Float): Float {
|
||||||
float coeff;
|
val water = (100.0f - getBodyFat(weight, impedance)) * 0.7f
|
||||||
float water = (100.0f - getBodyFat(weight, impedance)) * 0.7f;
|
val coeff = if (water < 50f) 1.02f else 0.98f
|
||||||
|
return coeff * water
|
||||||
if (water < 50) {
|
|
||||||
coeff = 1.02f;
|
|
||||||
} else {
|
|
||||||
coeff = 0.98f;
|
|
||||||
}
|
|
||||||
|
|
||||||
return coeff * water;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getBoneMass(float weight, float impedance) {
|
fun getBoneMass(weight: Float, impedance: Float): Float {
|
||||||
float boneMass;
|
val base = if (sex == 0) 0.245691014f else 0.18016894f
|
||||||
float base;
|
var boneMass = (base - (getLBMCoefficient(weight, impedance) * 0.05158f)) * -1.0f
|
||||||
|
|
||||||
if (sex == 0) {
|
boneMass = if (boneMass > 2.2f) boneMass + 0.1f else boneMass - 0.1f
|
||||||
base = 0.245691014f;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
base = 0.18016894f;
|
|
||||||
}
|
|
||||||
|
|
||||||
boneMass = (base - (getLBMCoefficient(weight, impedance) * 0.05158f)) * -1.0f;
|
|
||||||
|
|
||||||
if (boneMass > 2.2f) {
|
|
||||||
boneMass += 0.1f;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
boneMass -= 0.1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sex == 0 && boneMass > 5.1f) {
|
if (sex == 0 && boneMass > 5.1f) {
|
||||||
boneMass = 8.0f;
|
boneMass = 8.0f
|
||||||
}
|
} else if (sex == 1 && boneMass > 5.2f) {
|
||||||
else if (sex == 1 && boneMass > 5.2f) {
|
boneMass = 8.0f
|
||||||
boneMass = 8.0f;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return boneMass;
|
return boneMass
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getVisceralFat(float weight) {
|
fun getVisceralFat(weight: Float): Float {
|
||||||
float visceralFat = 0.0f;
|
var visceralFat = 0.0f
|
||||||
if (sex == 0) {
|
if (sex == 0) {
|
||||||
if (weight > (13.0f - (height * 0.5f)) * -1.0f) {
|
if (weight > (13.0f - (height * 0.5f)) * -1.0f) {
|
||||||
float subsubcalc = ((height * 1.45f) + (height * 0.1158f) * height) - 120.0f;
|
val subsubcalc = ((height * 1.45f) + (height * 0.1158f) * height) - 120.0f
|
||||||
float subcalc = weight * 500.0f / subsubcalc;
|
val subcalc = weight * 500.0f / subsubcalc
|
||||||
visceralFat = (subcalc - 6.0f) + (age * 0.07f);
|
visceralFat = (subcalc - 6.0f) + (age * 0.07f)
|
||||||
|
} else {
|
||||||
|
val subcalc = 0.691f + (height * -0.0024f) + (height * -0.0024f)
|
||||||
|
visceralFat = (((height * 0.027f) - (subcalc * weight)) * -1.0f) + (age * 0.07f) - age
|
||||||
}
|
}
|
||||||
else {
|
} else {
|
||||||
float subcalc = 0.691f + (height * -0.0024f) + (height * -0.0024f);
|
|
||||||
visceralFat = (((height * 0.027f) - (subcalc * weight)) * -1.0f) + (age * 0.07f) - age;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (height < weight * 1.6f) {
|
if (height < weight * 1.6f) {
|
||||||
float subcalc = ((height * 0.4f) - (height * (height * 0.0826f))) * -1.0f;
|
val subcalc = ((height * 0.4f) - (height * (height * 0.0826f))) * -1.0f
|
||||||
visceralFat = ((weight * 305.0f) / (subcalc + 48.0f)) - 2.9f + (age * 0.15f);
|
visceralFat = ((weight * 305.0f) / (subcalc + 48.0f)) - 2.9f + (age * 0.15f)
|
||||||
}
|
} else {
|
||||||
else {
|
val subcalc = 0.765f + height * -0.0015f
|
||||||
float subcalc = 0.765f + height * -0.0015f;
|
visceralFat = (((height * 0.143f) - (weight * subcalc)) * -1.0f) + (age * 0.15f) - 5.0f
|
||||||
visceralFat = (((height * 0.143f) - (weight * subcalc)) * -1.0f) + (age * 0.15f) - 5.0f;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return visceralFat
|
||||||
return visceralFat;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getBodyFat(float weight, float impedance) {
|
fun getBodyFat(weight: Float, impedance: Float): Float {
|
||||||
float bodyFat = 0.0f;
|
var lbmSub = 0.8f
|
||||||
float lbmSub = 0.8f;
|
|
||||||
|
|
||||||
if (sex == 0 && age <= 49) {
|
if (sex == 0 && age <= 49) {
|
||||||
lbmSub = 9.25f;
|
lbmSub = 9.25f
|
||||||
} else if (sex == 0 && age > 49) {
|
} else if (sex == 0 && age > 49) {
|
||||||
lbmSub = 7.25f;
|
lbmSub = 7.25f
|
||||||
}
|
}
|
||||||
|
|
||||||
float lbmCoeff = getLBMCoefficient(weight, impedance);
|
val lbmCoeff = getLBMCoefficient(weight, impedance)
|
||||||
float coeff = 1.0f;
|
var coeff = 1.0f
|
||||||
|
|
||||||
if (sex == 1 && weight < 61.0f) {
|
if (sex == 1 && weight < 61.0f) {
|
||||||
coeff = 0.98f;
|
coeff = 0.98f
|
||||||
}
|
} else if (sex == 0 && weight > 60.0f) {
|
||||||
else if (sex == 0 && weight > 60.0f) {
|
coeff = 0.96f
|
||||||
coeff = 0.96f;
|
|
||||||
|
|
||||||
if (height > 160.0f) {
|
if (height > 160.0f) {
|
||||||
coeff *= 1.03f;
|
coeff *= 1.03f
|
||||||
}
|
}
|
||||||
} else if (sex == 0 && weight < 50.0f) {
|
} else if (sex == 0 && weight < 50.0f) {
|
||||||
coeff = 1.02f;
|
coeff = 1.02f
|
||||||
|
|
||||||
if (height > 160.0f) {
|
if (height > 160.0f) {
|
||||||
coeff *= 1.03f;
|
coeff *= 1.03f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyFat = (1.0f - (((lbmCoeff - lbmSub) * coeff) / weight)) * 100.0f;
|
var bodyFat = (1.0f - (((lbmCoeff - lbmSub) * coeff) / weight)) * 100.0f
|
||||||
|
|
||||||
if (bodyFat > 63.0f) {
|
if (bodyFat > 63.0f) {
|
||||||
bodyFat = 75.0f;
|
bodyFat = 75.0f
|
||||||
}
|
}
|
||||||
|
return bodyFat
|
||||||
return bodyFat;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static float clamp(float v, float lo, float hi) {
|
|
||||||
return Math.max(lo, Math.min(hi, v));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,253 +1,246 @@
|
|||||||
/* Copyright (C) 2018 olie.xdev <olie.xdev@googlemail.com>
|
/*
|
||||||
|
* openScale
|
||||||
|
* Copyright (C) 2025 olie.xdev <olie.xdeveloper@googlemail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful,
|
* This program is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
package com.health.openscale.core.bluetooth.libs
|
||||||
|
|
||||||
package com.health.openscale.core.bluetooth.libs;
|
class OneByoneLib(// male = 1; female = 0
|
||||||
|
private val sex: Int,
|
||||||
public class OneByoneLib {
|
private val age: Int,
|
||||||
private int sex; // male = 1; female = 0
|
private val height: Float, // low activity = 0; medium activity = 1; high activity = 2
|
||||||
private int age;
|
private val peopleType: Int
|
||||||
private float height;
|
) {
|
||||||
private int peopleType; // low activity = 0; medium activity = 1; high activity = 2
|
fun getBMI(weight: Float): Float {
|
||||||
|
return weight / (((height * height) / 100.0f) / 100.0f)
|
||||||
public OneByoneLib(int sex, int age, float height, int peopleType) {
|
|
||||||
this.sex = sex;
|
|
||||||
this.age = age;
|
|
||||||
this.height = height;
|
|
||||||
this.peopleType = peopleType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getBMI(float weight) {
|
fun getLBM(weight: Float, bodyFat: Float): Float {
|
||||||
return weight / (((height * height) / 100.0f) / 100.0f);
|
return weight - (bodyFat / 100.0f * weight)
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getLBM(float weight, float bodyFat) {
|
fun getMuscle(weight: Float, impedanceValue: Float): Float {
|
||||||
return weight - (bodyFat / 100.0f * weight);
|
return ((height * height / impedanceValue * 0.401) + (sex * 3.825) - (age * 0.071) + 5.102).toFloat() / weight * 100.0f
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getMuscle(float weight, float impedanceValue){
|
fun getWater(bodyFat: Float): Float {
|
||||||
return (float)((height * height / impedanceValue * 0.401) + (sex * 3.825) - (age * 0.071) + 5.102) / weight * 100.0f;
|
val coeff: Float
|
||||||
}
|
val water = (100.0f - bodyFat) * 0.7f
|
||||||
|
|
||||||
public float getWater(float bodyFat) {
|
|
||||||
float coeff;
|
|
||||||
float water = (100.0f - bodyFat) * 0.7f;
|
|
||||||
|
|
||||||
if (water < 50) {
|
if (water < 50) {
|
||||||
coeff = 1.02f;
|
coeff = 1.02f
|
||||||
} else {
|
} else {
|
||||||
coeff = 0.98f;
|
coeff = 0.98f
|
||||||
}
|
}
|
||||||
|
|
||||||
return coeff * water;
|
return coeff * water
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getBoneMass(float weight, float impedanceValue) {
|
fun getBoneMass(weight: Float, impedanceValue: Float): Float {
|
||||||
float boneMass, sexConst , peopleCoeff = 0.0f;
|
var boneMass: Float
|
||||||
|
val sexConst: Float
|
||||||
|
var peopleCoeff = 0.0f
|
||||||
|
|
||||||
switch (peopleType) {
|
when (peopleType) {
|
||||||
case 0:
|
0 -> peopleCoeff = 1.0f
|
||||||
peopleCoeff = 1.0f;
|
1 -> peopleCoeff = 1.0427f
|
||||||
break;
|
2 -> peopleCoeff = 1.0958f
|
||||||
case 1:
|
|
||||||
peopleCoeff = 1.0427f;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
peopleCoeff = 1.0958f;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boneMass = (9.058f * (height / 100.0f) * (height / 100.0f) + 12.226f + (0.32f * weight)) - (0.0068f * impedanceValue);
|
boneMass =
|
||||||
|
(9.058f * (height / 100.0f) * (height / 100.0f) + 12.226f + (0.32f * weight)) - (0.0068f * impedanceValue)
|
||||||
|
|
||||||
if (sex == 1) { // male
|
if (sex == 1) { // male
|
||||||
sexConst = 3.49305f;
|
sexConst = 3.49305f
|
||||||
} else {
|
} else {
|
||||||
sexConst = 4.76325f;
|
sexConst = 4.76325f
|
||||||
}
|
}
|
||||||
|
|
||||||
boneMass = boneMass - sexConst - (age * 0.0542f) * peopleCoeff;
|
boneMass = boneMass - sexConst - (age * 0.0542f) * peopleCoeff
|
||||||
|
|
||||||
if (boneMass <= 2.2f) {
|
if (boneMass <= 2.2f) {
|
||||||
boneMass = boneMass - 0.1f;
|
boneMass = boneMass - 0.1f
|
||||||
} else {
|
} else {
|
||||||
boneMass = boneMass + 0.1f;
|
boneMass = boneMass + 0.1f
|
||||||
}
|
}
|
||||||
|
|
||||||
boneMass = boneMass * 0.05158f;
|
boneMass = boneMass * 0.05158f
|
||||||
|
|
||||||
if (0.5f > boneMass) {
|
if (0.5f > boneMass) {
|
||||||
return 0.5f;
|
return 0.5f
|
||||||
} else if (boneMass > 8.0f) {
|
} else if (boneMass > 8.0f) {
|
||||||
return 8.0f;
|
return 8.0f
|
||||||
}
|
}
|
||||||
|
|
||||||
return boneMass;
|
return boneMass
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getVisceralFat(float weight) {
|
fun getVisceralFat(weight: Float): Float {
|
||||||
float visceralFat;
|
val visceralFat: Float
|
||||||
|
|
||||||
if (sex == 1) {
|
if (sex == 1) {
|
||||||
if (height < ((1.6f * weight) + 63.0f)) {
|
if (height < ((1.6f * weight) + 63.0f)) {
|
||||||
visceralFat = (((weight * 305.0f) / (0.0826f * height * height - (0.4f * height) + 48.0f)) - 2.9f) + ((float)age * 0.15f);
|
visceralFat =
|
||||||
|
(((weight * 305.0f) / (0.0826f * height * height - (0.4f * height) + 48.0f)) - 2.9f) + (age.toFloat() * 0.15f)
|
||||||
|
|
||||||
if (peopleType == 0) {
|
if (peopleType == 0) {
|
||||||
return visceralFat;
|
return visceralFat
|
||||||
} else {
|
} else {
|
||||||
return subVisceralFat_A(visceralFat);
|
return subVisceralFat_A(visceralFat)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
visceralFat = (((float)age * 0.15f) + ((weight * (-0.0015f * height + 0.765f)) - height * 0.143f)) - 5.0f;
|
visceralFat =
|
||||||
|
((age.toFloat() * 0.15f) + ((weight * (-0.0015f * height + 0.765f)) - height * 0.143f)) - 5.0f
|
||||||
|
|
||||||
if (peopleType == 0) {
|
if (peopleType == 0) {
|
||||||
return visceralFat;
|
return visceralFat
|
||||||
} else {
|
} else {
|
||||||
return subVisceralFat_A(visceralFat);
|
return subVisceralFat_A(visceralFat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (((0.5f * height) - 13.0f) > weight) {
|
if (((0.5f * height) - 13.0f) > weight) {
|
||||||
visceralFat = (((float)age * 0.07f) + ((weight * (-0.0024f * height + 0.691f)) - (height * 0.027f))) - 10.5f;
|
visceralFat =
|
||||||
|
((age.toFloat() * 0.07f) + ((weight * (-0.0024f * height + 0.691f)) - (height * 0.027f))) - 10.5f
|
||||||
|
|
||||||
if (peopleType != 0) {
|
if (peopleType != 0) {
|
||||||
return subVisceralFat_A(visceralFat);
|
return subVisceralFat_A(visceralFat)
|
||||||
} else {
|
} else {
|
||||||
return visceralFat;
|
return visceralFat
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
visceralFat = (weight * 500.0f) / (((1.45f * height) + 0.1158f * height * height) - 120.0f) - 6.0f + ((float)age * 0.07f);
|
visceralFat =
|
||||||
|
(weight * 500.0f) / (((1.45f * height) + 0.1158f * height * height) - 120.0f) - 6.0f + (age.toFloat() * 0.07f)
|
||||||
|
|
||||||
if (peopleType == 0) {
|
if (peopleType == 0) {
|
||||||
return visceralFat;
|
return visceralFat
|
||||||
} else {
|
} else {
|
||||||
return subVisceralFat_A(visceralFat);
|
return subVisceralFat_A(visceralFat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private float subVisceralFat_A(float visceralFat) {
|
private fun subVisceralFat_A(visceralFat: Float): Float {
|
||||||
|
var visceralFat = visceralFat
|
||||||
if (peopleType != 0) {
|
if (peopleType != 0) {
|
||||||
if (10.0f <= visceralFat) {
|
if (10.0f <= visceralFat) {
|
||||||
|
return subVisceralFat_B(visceralFat)
|
||||||
return subVisceralFat_B(visceralFat);
|
|
||||||
} else {
|
} else {
|
||||||
visceralFat = visceralFat - 4.0f;
|
visceralFat = visceralFat - 4.0f
|
||||||
return visceralFat;
|
return visceralFat
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (10.0f > visceralFat) {
|
if (10.0f > visceralFat) {
|
||||||
visceralFat = visceralFat - 2.0f;
|
visceralFat = visceralFat - 2.0f
|
||||||
return visceralFat;
|
return visceralFat
|
||||||
} else {
|
} else {
|
||||||
return subVisceralFat_B(visceralFat);
|
return subVisceralFat_B(visceralFat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private float subVisceralFat_B(float visceralFat) {
|
private fun subVisceralFat_B(visceralFat: Float): Float {
|
||||||
|
var visceralFat = visceralFat
|
||||||
if (visceralFat < 10.0f) {
|
if (visceralFat < 10.0f) {
|
||||||
visceralFat = visceralFat * 0.85f;
|
visceralFat = visceralFat * 0.85f
|
||||||
return visceralFat;
|
return visceralFat
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
if (20.0f < visceralFat) {
|
if (20.0f < visceralFat) {
|
||||||
visceralFat = visceralFat * 0.85f;
|
visceralFat = visceralFat * 0.85f
|
||||||
return visceralFat;
|
return visceralFat
|
||||||
} else {
|
} else {
|
||||||
visceralFat = visceralFat * 0.8f;
|
visceralFat = visceralFat * 0.8f
|
||||||
return visceralFat;
|
return visceralFat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getBodyFat(float weight, float impedanceValue) {
|
fun getBodyFat(weight: Float, impedanceValue: Float): Float {
|
||||||
float bodyFatConst=0;
|
var bodyFatConst = 0f
|
||||||
|
|
||||||
if (impedanceValue >= 1200.0f) bodyFatConst = 8.16f;
|
if (impedanceValue >= 1200.0f) bodyFatConst = 8.16f
|
||||||
else if (impedanceValue >= 200.0f) bodyFatConst = 0.0068f * impedanceValue;
|
else if (impedanceValue >= 200.0f) bodyFatConst = 0.0068f * impedanceValue
|
||||||
else if (impedanceValue >= 50.0f) bodyFatConst = 1.36f;
|
else if (impedanceValue >= 50.0f) bodyFatConst = 1.36f
|
||||||
|
|
||||||
float peopleTypeCoeff, bodyVar, bodyFat;
|
val peopleTypeCoeff: Float
|
||||||
|
var bodyVar: Float
|
||||||
|
val bodyFat: Float
|
||||||
|
|
||||||
if (peopleType == 0) {
|
if (peopleType == 0) {
|
||||||
peopleTypeCoeff = 1.0f;
|
peopleTypeCoeff = 1.0f
|
||||||
} else {
|
} else {
|
||||||
if (peopleType == 1) {
|
if (peopleType == 1) {
|
||||||
peopleTypeCoeff = 1.0427f;
|
peopleTypeCoeff = 1.0427f
|
||||||
} else {
|
} else {
|
||||||
peopleTypeCoeff = 1.0958f;
|
peopleTypeCoeff = 1.0958f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyVar = (9.058f * height) / 100.0f;
|
bodyVar = (9.058f * height) / 100.0f
|
||||||
bodyVar = bodyVar * height;
|
bodyVar = bodyVar * height
|
||||||
bodyVar = bodyVar / 100.0f + 12.226f;
|
bodyVar = bodyVar / 100.0f + 12.226f
|
||||||
bodyVar = bodyVar + 0.32f * weight;
|
bodyVar = bodyVar + 0.32f * weight
|
||||||
bodyVar = bodyVar - bodyFatConst;
|
bodyVar = bodyVar - bodyFatConst
|
||||||
|
|
||||||
if (age > 0x31) {
|
if (age > 0x31) {
|
||||||
bodyFatConst = 7.25f;
|
bodyFatConst = 7.25f
|
||||||
|
|
||||||
if (sex == 1) {
|
if (sex == 1) {
|
||||||
bodyFatConst = 0.8f;
|
bodyFatConst = 0.8f
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bodyFatConst = 9.25f;
|
bodyFatConst = 9.25f
|
||||||
|
|
||||||
if (sex == 1) {
|
if (sex == 1) {
|
||||||
bodyFatConst = 0.8f;
|
bodyFatConst = 0.8f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyVar = bodyVar - bodyFatConst;
|
bodyVar = bodyVar - bodyFatConst
|
||||||
bodyVar = bodyVar - (age * 0.0542f);
|
bodyVar = bodyVar - (age * 0.0542f)
|
||||||
bodyVar = bodyVar * peopleTypeCoeff;
|
bodyVar = bodyVar * peopleTypeCoeff
|
||||||
|
|
||||||
if (sex != 0) {
|
if (sex != 0) {
|
||||||
if (61.0f > weight) {
|
if (61.0f > weight) {
|
||||||
bodyVar *= 0.98f;
|
bodyVar *= 0.98f
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (50.0f > weight) {
|
if (50.0f > weight) {
|
||||||
bodyVar *= 1.02f;
|
bodyVar *= 1.02f
|
||||||
}
|
}
|
||||||
|
|
||||||
if (weight > 60.0f) {
|
if (weight > 60.0f) {
|
||||||
bodyVar *= 0.96f;
|
bodyVar *= 0.96f
|
||||||
}
|
}
|
||||||
|
|
||||||
if (height > 160.0f) {
|
if (height > 160.0f) {
|
||||||
bodyVar *= 1.03f;
|
bodyVar *= 1.03f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyVar = bodyVar / weight;
|
bodyVar = bodyVar / weight
|
||||||
bodyFat = 100.0f * (1.0f - bodyVar);
|
bodyFat = 100.0f * (1.0f - bodyVar)
|
||||||
|
|
||||||
if (1.0f > bodyFat) {
|
if (1.0f > bodyFat) {
|
||||||
return 1.0f;
|
return 1.0f
|
||||||
} else {
|
} else {
|
||||||
if (bodyFat > 45.0f) {
|
if (bodyFat > 45.0f) {
|
||||||
return 45.0f;
|
return 45.0f
|
||||||
} else {
|
} else {
|
||||||
return bodyFat;
|
return bodyFat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,201 +1,207 @@
|
|||||||
package com.health.openscale.core.bluetooth.libs;
|
/*
|
||||||
|
* openScale
|
||||||
|
* Copyright (C) 2025 olie.xdev <olie.xdeveloper@googlemail.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.health.openscale.core.bluetooth.libs
|
||||||
|
|
||||||
// This class is similar to OneByoneLib, but the way measures are computer are slightly different
|
// This class is similar to OneByoneLib, but the way measures are computer are slightly different
|
||||||
public class OneByoneNewLib {
|
class OneByoneNewLib(
|
||||||
|
private val sex: Int,
|
||||||
private int sex;
|
private val age: Int,
|
||||||
private int age;
|
private val height: Float, // low activity = 0; medium activity = 1; high activity = 2
|
||||||
private float height;
|
private val peopleType: Int
|
||||||
private int peopleType; // low activity = 0; medium activity = 1; high activity = 2
|
) {
|
||||||
|
fun getBMI(weight: Float): Float {
|
||||||
public OneByoneNewLib(int sex, int age, float height, int peopleType) {
|
val bmi = weight / (((height * height) / 100.0f) / 100.0f)
|
||||||
this.sex = sex;
|
return getBounded(bmi, 10f, 90f)
|
||||||
this.age = age;
|
|
||||||
this.height = height;
|
|
||||||
this.peopleType = peopleType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getBMI(float weight) {
|
fun getLBM(weight: Float, impedance: Int): Float {
|
||||||
float bmi = weight / (((height * height) / 100.0f) / 100.0f);
|
var lbmCoeff = height / 100 * height / 100 * 9.058f
|
||||||
return getBounded(bmi, 10, 90);
|
lbmCoeff += 12.226.toFloat()
|
||||||
}
|
lbmCoeff += (weight * 0.32).toFloat()
|
||||||
|
lbmCoeff -= (impedance * 0.0068).toFloat()
|
||||||
public float getLBM(float weight, int impedance) {
|
lbmCoeff -= (age * 0.0542).toFloat()
|
||||||
float lbmCoeff = height / 100 * height / 100 * 9.058F;
|
return lbmCoeff
|
||||||
lbmCoeff += 12.226;
|
|
||||||
lbmCoeff += weight * 0.32;
|
|
||||||
lbmCoeff -= impedance * 0.0068;
|
|
||||||
lbmCoeff -= age * 0.0542;
|
|
||||||
return lbmCoeff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getBMMRCoeff(weight: Float): Float {
|
||||||
public float getBMMRCoeff(float weight){
|
var bmmrCoeff = 20
|
||||||
int bmmrCoeff = 20;
|
if (sex == 1) {
|
||||||
if(sex == 1){
|
bmmrCoeff = 21
|
||||||
bmmrCoeff = 21;
|
if (age < 0xd) {
|
||||||
if(age < 0xd){
|
bmmrCoeff = 36
|
||||||
bmmrCoeff = 36;
|
} else if (age < 0x10) {
|
||||||
} else if(age < 0x10){
|
bmmrCoeff = 30
|
||||||
bmmrCoeff = 30;
|
} else if (age < 0x12) {
|
||||||
} else if(age < 0x12){
|
bmmrCoeff = 26
|
||||||
bmmrCoeff = 26;
|
} else if (age < 0x1e) {
|
||||||
} else if(age < 0x1e){
|
bmmrCoeff = 23
|
||||||
bmmrCoeff = 23;
|
} else if (age >= 0x32) {
|
||||||
} else if (age >= 0x32){
|
bmmrCoeff = 20
|
||||||
bmmrCoeff = 20;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if(age < 0xd){
|
if (age < 0xd) {
|
||||||
bmmrCoeff = 34;
|
bmmrCoeff = 34
|
||||||
} else if(age < 0x10){
|
} else if (age < 0x10) {
|
||||||
bmmrCoeff = 29;
|
bmmrCoeff = 29
|
||||||
} else if(age < 0x12){
|
} else if (age < 0x12) {
|
||||||
bmmrCoeff = 24;
|
bmmrCoeff = 24
|
||||||
} else if(age < 0x1e){
|
} else if (age < 0x1e) {
|
||||||
bmmrCoeff = 22;
|
bmmrCoeff = 22
|
||||||
} else if (age >= 0x32){
|
} else if (age >= 0x32) {
|
||||||
bmmrCoeff = 19;
|
bmmrCoeff = 19
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bmmrCoeff;
|
return bmmrCoeff.toFloat()
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getBMMR(float weight){
|
fun getBMMR(weight: Float): Float {
|
||||||
float bmmr;
|
var bmmr: Float
|
||||||
if(sex == 1){
|
if (sex == 1) {
|
||||||
bmmr = (weight * 14.916F + 877.8F) - height * 0.726F;
|
bmmr = (weight * 14.916f + 877.8f) - height * 0.726f
|
||||||
bmmr -= age * 8.976;
|
bmmr -= (age * 8.976).toFloat()
|
||||||
} else {
|
} else {
|
||||||
bmmr = (weight * 10.2036F + 864.6F) - height * 0.39336F;
|
bmmr = (weight * 10.2036f + 864.6f) - height * 0.39336f
|
||||||
bmmr -= age * 6.204;
|
bmmr -= (age * 6.204).toFloat()
|
||||||
}
|
}
|
||||||
|
|
||||||
return getBounded(bmmr, 500, 1000);
|
return getBounded(bmmr, 500f, 1000f)
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getBodyFatPercentage(float weight, int impedance) {
|
fun getBodyFatPercentage(weight: Float, impedance: Int): Float {
|
||||||
float bodyFat = getLBM(weight, impedance);
|
var bodyFat = getLBM(weight, impedance)
|
||||||
|
|
||||||
float bodyFatConst;
|
val bodyFatConst: Float
|
||||||
if (sex == 0) {
|
if (sex == 0) {
|
||||||
if (age < 0x32) {
|
if (age < 0x32) {
|
||||||
bodyFatConst = 9.25F;
|
bodyFatConst = 9.25f
|
||||||
} else {
|
} else {
|
||||||
bodyFatConst = 7.25F;
|
bodyFatConst = 7.25f
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bodyFatConst = 0.8F;
|
bodyFatConst = 0.8f
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyFat -= bodyFatConst;
|
bodyFat -= bodyFatConst
|
||||||
|
|
||||||
if (sex == 0){
|
if (sex == 0) {
|
||||||
if (weight < 50){
|
if (weight < 50) {
|
||||||
bodyFat *= 1.02;
|
bodyFat *= 1.02.toFloat()
|
||||||
} else if(weight > 60){
|
} else if (weight > 60) {
|
||||||
bodyFat *= 0.96;
|
bodyFat *= 0.96.toFloat()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(height > 160){
|
if (height > 160) {
|
||||||
bodyFat *= 1.03;
|
bodyFat *= 1.03.toFloat()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (weight < 61){
|
if (weight < 61) {
|
||||||
bodyFat *= 0.98;
|
bodyFat *= 0.98.toFloat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 100 * (1 - bodyFat / weight);
|
return 100 * (1 - bodyFat / weight)
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getBoneMass(float weight, int impedance){
|
fun getBoneMass(weight: Float, impedance: Int): Float {
|
||||||
float lbmCoeff = getLBM(weight, impedance);
|
val lbmCoeff = getLBM(weight, impedance)
|
||||||
|
|
||||||
float boneMassConst;
|
var boneMassConst: Float
|
||||||
if(sex == 1){
|
if (sex == 1) {
|
||||||
boneMassConst = 0.18016894F;
|
boneMassConst = 0.18016894f
|
||||||
} else {
|
} else {
|
||||||
boneMassConst = 0.245691014F;
|
boneMassConst = 0.245691014f
|
||||||
}
|
}
|
||||||
|
|
||||||
boneMassConst = lbmCoeff * 0.05158F - boneMassConst;
|
boneMassConst = lbmCoeff * 0.05158f - boneMassConst
|
||||||
float boneMass;
|
val boneMass: Float
|
||||||
if(boneMassConst <= 2.2){
|
if (boneMassConst <= 2.2) {
|
||||||
boneMass = boneMassConst - 0.1F;
|
boneMass = boneMassConst - 0.1f
|
||||||
} else {
|
} else {
|
||||||
boneMass = boneMassConst + 0.1F;
|
boneMass = boneMassConst + 0.1f
|
||||||
}
|
}
|
||||||
|
|
||||||
return getBounded(boneMass, 0.5F, 8);
|
return getBounded(boneMass, 0.5f, 8f)
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getMuscleMass(float weight, int impedance){
|
fun getMuscleMass(weight: Float, impedance: Int): Float {
|
||||||
float muscleMass = weight - getBodyFatPercentage(weight, impedance) * 0.01F * weight;
|
var muscleMass = weight - getBodyFatPercentage(weight, impedance) * 0.01f * weight
|
||||||
muscleMass -= getBoneMass(weight, impedance);
|
muscleMass -= getBoneMass(weight, impedance)
|
||||||
return getBounded(muscleMass, 10, 120);
|
return getBounded(muscleMass, 10f, 120f)
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getSkeletonMusclePercentage(float weight, int impedance){
|
fun getSkeletonMusclePercentage(weight: Float, impedance: Int): Float {
|
||||||
float skeletonMuscleMass = getWaterPercentage(weight, impedance);
|
var skeletonMuscleMass = getWaterPercentage(weight, impedance)
|
||||||
skeletonMuscleMass *= weight;
|
skeletonMuscleMass *= weight
|
||||||
skeletonMuscleMass *= 0.8422F * 0.01F;
|
skeletonMuscleMass *= 0.8422f * 0.01f
|
||||||
skeletonMuscleMass -= 2.9903;
|
skeletonMuscleMass -= 2.9903.toFloat()
|
||||||
skeletonMuscleMass /= weight;
|
skeletonMuscleMass /= weight
|
||||||
return skeletonMuscleMass * 100;
|
return skeletonMuscleMass * 100
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getVisceralFat(float weight){
|
fun getVisceralFat(weight: Float): Float {
|
||||||
float visceralFat;
|
val visceralFat: Float
|
||||||
if (sex == 1) {
|
if (sex == 1) {
|
||||||
if (height < weight * 1.6 + 63.0) {
|
if (height < weight * 1.6 + 63.0) {
|
||||||
visceralFat =
|
visceralFat =
|
||||||
age * 0.15F + ((weight * 305.0F) /((height * 0.0826F * height - height * 0.4F) + 48.0F) - 2.9F);
|
age * 0.15f + ((weight * 305.0f) / ((height * 0.0826f * height - height * 0.4f) + 48.0f) - 2.9f)
|
||||||
|
} else {
|
||||||
|
visceralFat =
|
||||||
|
age * 0.15f + (weight * (height * -0.0015f + 0.765f) - height * 0.143f) - 5.0f
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
visceralFat = age * 0.15F + (weight * (height * -0.0015F + 0.765F) - height * 0.143F) - 5.0F;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (weight <= height * 0.5 - 13.0) {
|
|
||||||
visceralFat = age * 0.07F + (weight * (height * -0.0024F + 0.691F) - height * 0.027F) - 10.5F;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
visceralFat = age * 0.07F + ((weight * 500.0F) / ((height * 1.45F + height * 0.1158F * height) - 120.0F) - 6.0F);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return getBounded(visceralFat, 1, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getWaterPercentage(float weight, int impedance){
|
|
||||||
float waterPercentage = (100 - getBodyFatPercentage(weight, impedance)) * 0.7F;
|
|
||||||
if (waterPercentage > 50){
|
|
||||||
waterPercentage *= 0.98;
|
|
||||||
} else {
|
} else {
|
||||||
waterPercentage *= 1.02;
|
if (weight <= height * 0.5 - 13.0) {
|
||||||
|
visceralFat =
|
||||||
|
age * 0.07f + (weight * (height * -0.0024f + 0.691f) - height * 0.027f) - 10.5f
|
||||||
|
} else {
|
||||||
|
visceralFat =
|
||||||
|
age * 0.07f + ((weight * 500.0f) / ((height * 1.45f + height * 0.1158f * height) - 120.0f) - 6.0f)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return getBounded(waterPercentage, 35, 75);
|
return getBounded(visceralFat, 1f, 50f)
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getProteinPercentage(float weight, int impedance){
|
fun getWaterPercentage(weight: Float, impedance: Int): Float {
|
||||||
return (
|
var waterPercentage = (100 - getBodyFatPercentage(weight, impedance)) * 0.7f
|
||||||
(100.0F - getBodyFatPercentage(weight, impedance))
|
if (waterPercentage > 50) {
|
||||||
- getWaterPercentage(weight, impedance) * 1.08F
|
waterPercentage *= 0.98.toFloat()
|
||||||
|
} else {
|
||||||
|
waterPercentage *= 1.02.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
return getBounded(waterPercentage, 35f, 75f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getProteinPercentage(weight: Float, impedance: Int): Float {
|
||||||
|
return (((100.0f - getBodyFatPercentage(weight, impedance))
|
||||||
|
- getWaterPercentage(weight, impedance) * 1.08f
|
||||||
)
|
)
|
||||||
- (getBoneMass(weight, impedance) / weight) * 100.0F;
|
- (getBoneMass(weight, impedance) / weight) * 100.0f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private float getBounded(float value, float lowerBound, float upperBound){
|
private fun getBounded(value: Float, lowerBound: Float, upperBound: Float): Float {
|
||||||
if(value < lowerBound){
|
if (value < lowerBound) {
|
||||||
return lowerBound;
|
return lowerBound
|
||||||
} else if (value > upperBound){
|
} else if (value > upperBound) {
|
||||||
return upperBound;
|
return upperBound
|
||||||
}
|
}
|
||||||
return value;
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,147 +1,126 @@
|
|||||||
/* Copyright (C) 2019 olie.xdev <olie.xdev@googlemail.com>
|
/*
|
||||||
|
* openScale
|
||||||
|
* Copyright (C) 2025 olie.xdev <olie.xdeveloper@googlemail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful,
|
* This program is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package com.health.openscale.core.bluetooth.libs;
|
package com.health.openscale.core.bluetooth.libs
|
||||||
|
|
||||||
public class SoehnleLib {
|
class SoehnleLib(// male = 1; female = 0
|
||||||
private boolean isMale; // male = 1; female = 0
|
private val isMale: Boolean,
|
||||||
private int age;
|
private val age: Int,
|
||||||
private float height;
|
private val height: Float,
|
||||||
private int activityLevel;
|
private val activityLevel: Int
|
||||||
|
) {
|
||||||
|
fun getFat(weight: Float, imp50: Float): Float { // in %
|
||||||
|
var activityCorrFac = 0.0f
|
||||||
|
|
||||||
public SoehnleLib(boolean isMale, int age, float height, int activityLevel) {
|
when (activityLevel) {
|
||||||
this.isMale = isMale;
|
4 -> {
|
||||||
this.age = age;
|
|
||||||
this.height = height;
|
|
||||||
this.activityLevel = activityLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getFat(final float weight, final float imp50) { // in %
|
|
||||||
float activityCorrFac = 0.0f;
|
|
||||||
|
|
||||||
switch (activityLevel) {
|
|
||||||
case 4: {
|
|
||||||
if (isMale) {
|
if (isMale) {
|
||||||
activityCorrFac = 2.5f;
|
activityCorrFac = 2.5f
|
||||||
|
} else {
|
||||||
|
activityCorrFac = 2.3f
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
activityCorrFac = 2.3f;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case 5: {
|
|
||||||
|
5 -> {
|
||||||
if (isMale) {
|
if (isMale) {
|
||||||
activityCorrFac = 4.3f;
|
activityCorrFac = 4.3f
|
||||||
|
} else {
|
||||||
|
activityCorrFac = 4.1f
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
activityCorrFac = 4.1f;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float sexCorrFac;
|
val sexCorrFac: Float
|
||||||
float activitySexDiv;
|
val activitySexDiv: Float
|
||||||
|
|
||||||
if (isMale) {
|
if (isMale) {
|
||||||
sexCorrFac = 0.250f;
|
sexCorrFac = 0.250f
|
||||||
activitySexDiv = 65.5f;
|
activitySexDiv = 65.5f
|
||||||
}
|
} else {
|
||||||
else {
|
sexCorrFac = 0.214f
|
||||||
sexCorrFac = 0.214f;
|
activitySexDiv = 55.1f
|
||||||
activitySexDiv = 55.1f;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 1.847f * weight * 10000.0f / (height * height) + sexCorrFac * age + 0.062f * imp50 - (activitySexDiv - activityCorrFac);
|
return 1.847f * weight * 10000.0f / (height * height) + sexCorrFac * age + 0.062f * imp50 - (activitySexDiv - activityCorrFac)
|
||||||
}
|
}
|
||||||
|
|
||||||
public float computeBodyMassIndex(final float weight) {
|
fun computeBodyMassIndex(weight: Float): Float {
|
||||||
return 10000.0f * weight / (height * height);
|
return 10000.0f * weight / (height * height)
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getWater(final float weight, final float imp50) { // in %
|
fun getWater(weight: Float, imp50: Float): Float { // in %
|
||||||
float activityCorrFac = 0.0f;
|
var activityCorrFac = 0.0f
|
||||||
|
|
||||||
switch (activityLevel) {
|
when (activityLevel) {
|
||||||
case 1:
|
1, 2, 3 -> {
|
||||||
case 2:
|
|
||||||
case 3: {
|
|
||||||
if (isMale) {
|
if (isMale) {
|
||||||
activityCorrFac = 2.83f;
|
activityCorrFac = 2.83f
|
||||||
|
} else {
|
||||||
|
activityCorrFac = 0.0f
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
activityCorrFac = 0.0f;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case 4: {
|
|
||||||
|
4 -> {
|
||||||
if (isMale) {
|
if (isMale) {
|
||||||
activityCorrFac = 3.93f;
|
activityCorrFac = 3.93f
|
||||||
|
} else {
|
||||||
|
activityCorrFac = 0.4f
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
activityCorrFac = 0.4f;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case 5: {
|
|
||||||
|
5 -> {
|
||||||
if (isMale) {
|
if (isMale) {
|
||||||
activityCorrFac = 5.33f;
|
activityCorrFac = 5.33f
|
||||||
|
} else {
|
||||||
|
activityCorrFac = 1.4f
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
activityCorrFac = 1.4f;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (0.3674f * height * height / imp50 + 0.17530f * weight - 0.11f * age + (6.53f + activityCorrFac)) / weight * 100.0f;
|
return (0.3674f * height * height / imp50 + 0.17530f * weight - 0.11f * age + (6.53f + activityCorrFac)) / weight * 100.0f
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getMuscle(final float weight, final float imp50, final float imp5) { // in %
|
fun getMuscle(weight: Float, imp50: Float, imp5: Float): Float { // in %
|
||||||
float activityCorrFac = 0.0f;
|
var activityCorrFac = 0.0f
|
||||||
|
|
||||||
switch (activityLevel) {
|
when (activityLevel) {
|
||||||
case 1:
|
1, 2, 3 -> {
|
||||||
case 2:
|
|
||||||
case 3: {
|
|
||||||
if (isMale) {
|
if (isMale) {
|
||||||
activityCorrFac = 3.6224f;
|
activityCorrFac = 3.6224f
|
||||||
|
} else {
|
||||||
|
activityCorrFac = 0.0f
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
activityCorrFac = 0.0f;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case 4: {
|
|
||||||
|
4 -> {
|
||||||
if (isMale) {
|
if (isMale) {
|
||||||
activityCorrFac = 4.3904f;
|
activityCorrFac = 4.3904f
|
||||||
|
} else {
|
||||||
|
activityCorrFac = 0.0f
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
activityCorrFac = 0.0f;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case 5: {
|
|
||||||
|
5 -> {
|
||||||
if (isMale) {
|
if (isMale) {
|
||||||
activityCorrFac = 5.4144f;
|
activityCorrFac = 5.4144f
|
||||||
|
} else {
|
||||||
|
activityCorrFac = 1.664f
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
activityCorrFac = 1.664f;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ((0.47027f / imp50 - 0.24196f / imp5) * height * height + 0.13796f * weight - 0.1152f * age + (5.12f + activityCorrFac)) / weight * 100.0f;
|
return ((0.47027f / imp50 - 0.24196f / imp5) * height * height + 0.13796f * weight - 0.1152f * age + (5.12f + activityCorrFac)) / weight * 100.0f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,77 +1,78 @@
|
|||||||
/* Copyright (C) 2018 Maks Verver <maks@verver.ch>
|
/*
|
||||||
* 2019 olie.xdev <olie.xdev@googlemail.com>
|
* openScale
|
||||||
|
* Copyright (C) 2018 Maks Verver <maks@verver.ch>
|
||||||
|
* 2025 olie.xdev <olie.xdeveloper@googlemail.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful,
|
* This program is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package com.health.openscale.core.bluetooth.libs;
|
package com.health.openscale.core.bluetooth.libs
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class with static helper methods. This is a separate class for testing purposes.
|
* Class with static helper methods. This is a separate class for testing purposes.
|
||||||
*/
|
*/
|
||||||
public class TrisaBodyAnalyzeLib {
|
class TrisaBodyAnalyzeLib(sex: Int, private val ageYears: Int, private val heightCm: Float) {
|
||||||
|
private val isMale: Boolean
|
||||||
|
|
||||||
private boolean isMale;
|
init {
|
||||||
private int ageYears;
|
isMale = if (sex == 1) true else false // male = 1; female = 0
|
||||||
private float heightCm;
|
|
||||||
|
|
||||||
public TrisaBodyAnalyzeLib(int sex, int age, float height) {
|
|
||||||
isMale = sex == 1 ? true : false; // male = 1; female = 0
|
|
||||||
ageYears = age;
|
|
||||||
heightCm = height;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getBMI(float weightKg) {
|
fun getBMI(weightKg: Float): Float {
|
||||||
return weightKg * 1e4f / (heightCm * heightCm);
|
return weightKg * 1e4f / (heightCm * heightCm)
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getWater(float weightKg, float impedance) {
|
fun getWater(weightKg: Float, impedance: Float): Float {
|
||||||
float bmi = getBMI(weightKg);
|
val bmi = getBMI(weightKg)
|
||||||
|
|
||||||
float water = isMale
|
val water = if (isMale)
|
||||||
? 87.51f + (-1.162f * bmi - 0.00813f * impedance + 0.07594f * ageYears)
|
87.51f + (-1.162f * bmi - 0.00813f * impedance + 0.07594f * ageYears)
|
||||||
: 77.721f + (-1.148f * bmi - 0.00573f * impedance + 0.06448f * ageYears);
|
else
|
||||||
|
77.721f + (-1.148f * bmi - 0.00573f * impedance + 0.06448f * ageYears)
|
||||||
|
|
||||||
return water;
|
return water
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getFat(float weightKg, float impedance) {
|
fun getFat(weightKg: Float, impedance: Float): Float {
|
||||||
float bmi = getBMI(weightKg);
|
val bmi = getBMI(weightKg)
|
||||||
|
|
||||||
float fat = isMale
|
val fat = if (isMale)
|
||||||
? bmi * (1.479f + 4.4e-4f * impedance) + 0.1f * ageYears - 21.764f
|
bmi * (1.479f + 4.4e-4f * impedance) + 0.1f * ageYears - 21.764f
|
||||||
: bmi * (1.506f + 3.908e-4f * impedance) + 0.1f * ageYears - 12.834f;
|
else
|
||||||
|
bmi * (1.506f + 3.908e-4f * impedance) + 0.1f * ageYears - 12.834f
|
||||||
|
|
||||||
return fat;
|
return fat
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getMuscle(float weightKg, float impedance) {
|
fun getMuscle(weightKg: Float, impedance: Float): Float {
|
||||||
float bmi = getBMI(weightKg);
|
val bmi = getBMI(weightKg)
|
||||||
|
|
||||||
float muscle = isMale
|
val muscle = if (isMale)
|
||||||
? 74.627f + (-0.811f * bmi - 0.00565f * impedance - 0.367f * ageYears)
|
74.627f + (-0.811f * bmi - 0.00565f * impedance - 0.367f * ageYears)
|
||||||
: 57.0f + (-0.694f * bmi - 0.00344f * impedance - 0.255f * ageYears);
|
else
|
||||||
|
57.0f + (-0.694f * bmi - 0.00344f * impedance - 0.255f * ageYears)
|
||||||
|
|
||||||
return muscle;
|
return muscle
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getBone(float weightKg, float impedance) {
|
fun getBone(weightKg: Float, impedance: Float): Float {
|
||||||
float bmi = getBMI(weightKg);
|
val bmi = getBMI(weightKg)
|
||||||
|
|
||||||
float bone = isMale
|
val bone = if (isMale)
|
||||||
? 7.829f + (-0.0855f * bmi - 5.92e-4f * impedance - 0.0389f * ageYears)
|
7.829f + (-0.0855f * bmi - 5.92e-4f * impedance - 0.0389f * ageYears)
|
||||||
: 7.98f + (-0.0973f * bmi - 4.84e-4f * impedance - 0.036f * ageYears);
|
else
|
||||||
|
7.98f + (-0.0973f * bmi - 4.84e-4f * impedance - 0.036f * ageYears)
|
||||||
|
|
||||||
return bone;
|
return bone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,158 +15,157 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package com.health.openscale.core.bluetooth.libs;
|
package com.health.openscale.core.bluetooth.libs
|
||||||
|
|
||||||
|
import com.health.openscale.core.data.ActivityLevel
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
|
||||||
import com.health.openscale.core.data.ActivityLevel;
|
class YunmaiLib(// male = 1; female = 0
|
||||||
|
private val sex: Int, private val height: Float, activityLevel: ActivityLevel
|
||||||
|
) {
|
||||||
|
private val fitnessBodyType: Boolean
|
||||||
|
|
||||||
public class YunmaiLib {
|
init {
|
||||||
private int sex; // male = 1; female = 0
|
this.fitnessBodyType = toYunmaiActivityLevel(activityLevel) == 1
|
||||||
private float height;
|
|
||||||
private boolean fitnessBodyType;
|
|
||||||
|
|
||||||
static public int toYunmaiActivityLevel(ActivityLevel activityLevel) {
|
|
||||||
switch (activityLevel) {
|
|
||||||
case HEAVY:
|
|
||||||
case EXTREME:
|
|
||||||
return 1;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public YunmaiLib(int sex, float height, ActivityLevel activityLevel) {
|
fun getWater(bodyFat: Float): Float {
|
||||||
this.sex = sex;
|
return ((100.0f - bodyFat) * 0.726f * 100.0f + 0.5f) / 100.0f
|
||||||
this.height = height;
|
|
||||||
this.fitnessBodyType = YunmaiLib.toYunmaiActivityLevel(activityLevel) == 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getWater(float bodyFat) {
|
fun getFat(age: Int, weight: Float, resistance: Int): Float {
|
||||||
return ((100.0f - bodyFat) * 0.726f * 100.0f + 0.5f) / 100.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getFat(int age, float weight, int resistance) {
|
|
||||||
// for < 0x1e version devices
|
// for < 0x1e version devices
|
||||||
float fat;
|
var fat: Float
|
||||||
|
|
||||||
float r = (resistance - 100.0f) / 100.0f;
|
var r = (resistance - 100.0f) / 100.0f
|
||||||
float h = height / 100.0f;
|
val h = height / 100.0f
|
||||||
|
|
||||||
if (r >= 1) {
|
if (r >= 1) {
|
||||||
r = (float)Math.sqrt(r);
|
r = sqrt(r.toDouble()).toFloat()
|
||||||
}
|
}
|
||||||
|
|
||||||
fat = (weight * 1.5f / h / h) + (age * 0.08f);
|
fat = (weight * 1.5f / h / h) + (age * 0.08f)
|
||||||
if (this.sex == 1) {
|
if (this.sex == 1) {
|
||||||
fat -= 10.8f;
|
fat -= 10.8f
|
||||||
}
|
}
|
||||||
|
|
||||||
fat = (fat - 7.4f) + r;
|
fat = (fat - 7.4f) + r
|
||||||
|
|
||||||
if (fat < 5.0f || fat > 75.0f) {
|
if (fat < 5.0f || fat > 75.0f) {
|
||||||
fat = 0.0f;
|
fat = 0.0f
|
||||||
}
|
}
|
||||||
|
|
||||||
return fat;
|
return fat
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getMuscle(float bodyFat) {
|
fun getMuscle(bodyFat: Float): Float {
|
||||||
float muscle;
|
var muscle: Float
|
||||||
muscle = (100.0f - bodyFat) * 0.67f;
|
muscle = (100.0f - bodyFat) * 0.67f
|
||||||
|
|
||||||
if (this.fitnessBodyType) {
|
if (this.fitnessBodyType) {
|
||||||
muscle = (100.0f - bodyFat) * 0.7f;
|
muscle = (100.0f - bodyFat) * 0.7f
|
||||||
}
|
}
|
||||||
|
|
||||||
muscle = ((muscle * 100.0f) + 0.5f) / 100.0f;
|
muscle = ((muscle * 100.0f) + 0.5f) / 100.0f
|
||||||
|
|
||||||
return muscle;
|
return muscle
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getSkeletalMuscle(float bodyFat) {
|
fun getSkeletalMuscle(bodyFat: Float): Float {
|
||||||
float muscle;
|
var muscle: Float
|
||||||
|
|
||||||
muscle = (100.0f - bodyFat) * 0.53f;
|
muscle = (100.0f - bodyFat) * 0.53f
|
||||||
if (this.fitnessBodyType) {
|
if (this.fitnessBodyType) {
|
||||||
muscle = (100.0f - bodyFat) * 0.6f;
|
muscle = (100.0f - bodyFat) * 0.6f
|
||||||
}
|
}
|
||||||
|
|
||||||
muscle = ((muscle * 100.0f) + 0.5f) / 100.0f;
|
muscle = ((muscle * 100.0f) + 0.5f) / 100.0f
|
||||||
|
|
||||||
return muscle;
|
return muscle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public float getBoneMass(float muscle, float weight) {
|
fun getBoneMass(muscle: Float, weight: Float): Float {
|
||||||
float boneMass;
|
var boneMass: Float
|
||||||
|
|
||||||
float h = height - 170.0f;
|
val h = height - 170.0f
|
||||||
|
|
||||||
if (sex == 1) {
|
if (sex == 1) {
|
||||||
boneMass = ((weight * (muscle / 100.0f) * 4.0f) / 7.0f * 0.22f * 0.6f) + (h / 100.0f);
|
boneMass = ((weight * (muscle / 100.0f) * 4.0f) / 7.0f * 0.22f * 0.6f) + (h / 100.0f)
|
||||||
} else {
|
} else {
|
||||||
boneMass = ((weight * (muscle / 100.0f) * 4.0f) / 7.0f * 0.34f * 0.45f) + (h / 100.0f);
|
boneMass = ((weight * (muscle / 100.0f) * 4.0f) / 7.0f * 0.34f * 0.45f) + (h / 100.0f)
|
||||||
}
|
}
|
||||||
|
|
||||||
boneMass = ((boneMass * 10.0f) + 0.5f) / 10.0f;
|
boneMass = ((boneMass * 10.0f) + 0.5f) / 10.0f
|
||||||
|
|
||||||
return boneMass;
|
return boneMass
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getLeanBodyMass(float weight, float bodyFat) {
|
fun getLeanBodyMass(weight: Float, bodyFat: Float): Float {
|
||||||
return weight * (100.0f - bodyFat) / 100.0f;
|
return weight * (100.0f - bodyFat) / 100.0f
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getVisceralFat(float bodyFat, int age) {
|
fun getVisceralFat(bodyFat: Float, age: Int): Float {
|
||||||
float f = bodyFat;
|
var f = bodyFat
|
||||||
int a = (age < 18 || age > 120) ? 18 : age;
|
val a = if (age < 18 || age > 120) 18 else age
|
||||||
|
|
||||||
float vf;
|
val vf: Float
|
||||||
if (!fitnessBodyType) {
|
if (!fitnessBodyType) {
|
||||||
if (sex == 1) {
|
if (sex == 1) {
|
||||||
if (a < 40) {
|
if (a < 40) {
|
||||||
f -= 21.0f;
|
f -= 21.0f
|
||||||
} else if (a < 60) {
|
} else if (a < 60) {
|
||||||
f -= 22.0f;
|
f -= 22.0f
|
||||||
} else {
|
} else {
|
||||||
f -= 24.0f;
|
f -= 24.0f
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (a < 40) {
|
if (a < 40) {
|
||||||
f -= 34.0f;
|
f -= 34.0f
|
||||||
} else if (a < 60) {
|
} else if (a < 60) {
|
||||||
f -= 35.0f;
|
f -= 35.0f
|
||||||
} else {
|
} else {
|
||||||
f -= 36.0f;
|
f -= 36.0f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float d = sex == 1 ? 1.4f : 1.8f;
|
var d = if (sex == 1) 1.4f else 1.8f
|
||||||
if (f > 0.0f) {
|
if (f > 0.0f) {
|
||||||
d = 1.1f;
|
d = 1.1f
|
||||||
}
|
}
|
||||||
|
|
||||||
vf = (f / d) + 9.5f;
|
vf = (f / d) + 9.5f
|
||||||
if (vf < 1.0f) {
|
if (vf < 1.0f) {
|
||||||
return 1.0f;
|
return 1.0f
|
||||||
}
|
}
|
||||||
if (vf > 30.0f) {
|
if (vf > 30.0f) {
|
||||||
return 30.0f;
|
return 30.0f
|
||||||
}
|
}
|
||||||
return vf;
|
return vf
|
||||||
} else {
|
} else {
|
||||||
if (bodyFat > 15.0f) {
|
if (bodyFat > 15.0f) {
|
||||||
vf = (bodyFat - 15.0f) / 1.1f + 12.0f;
|
vf = (bodyFat - 15.0f) / 1.1f + 12.0f
|
||||||
} else {
|
} else {
|
||||||
vf = -1 * (15.0f - bodyFat) / 1.4f + 12.0f;
|
vf = -1 * (15.0f - bodyFat) / 1.4f + 12.0f
|
||||||
}
|
}
|
||||||
if (vf < 1.0f) {
|
if (vf < 1.0f) {
|
||||||
return 1.0f;
|
return 1.0f
|
||||||
}
|
}
|
||||||
if (vf > 9.0f) {
|
if (vf > 9.0f) {
|
||||||
return 9.0f;
|
return 9.0f
|
||||||
|
}
|
||||||
|
return vf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun toYunmaiActivityLevel(activityLevel: ActivityLevel): Int {
|
||||||
|
when (activityLevel) {
|
||||||
|
ActivityLevel.HEAVY, ActivityLevel.EXTREME -> return 1
|
||||||
|
else -> return 0
|
||||||
}
|
}
|
||||||
return vf;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,214 @@
|
|||||||
|
/*
|
||||||
|
* openScale
|
||||||
|
* Copyright (C) 2025 olie.xdev <olie.xdeveloper@googlemail.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.health.openscale.core.bluetooth.libs
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for [MiScaleLib] (current implementation).
|
||||||
|
*
|
||||||
|
* - Three regression fixtures use the exact outputs printed from the current code
|
||||||
|
* to guard against accidental changes.
|
||||||
|
* - Behavioral tests verify important branches without brittle hard-coded numbers.
|
||||||
|
*/
|
||||||
|
class MiScaleLibTest {
|
||||||
|
|
||||||
|
private val EPS = 1e-3f // general float tolerance
|
||||||
|
|
||||||
|
// --- Simple BMI checks ----------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun bmi_isComputedCorrectly_forTypicalMale() {
|
||||||
|
// Given
|
||||||
|
val lib = MiScaleLib(/* sex=male */ 1, /* age */ 30, /* height cm */ 180f)
|
||||||
|
val weight = 80f
|
||||||
|
|
||||||
|
// When
|
||||||
|
val bmi = lib.getBMI(weight)
|
||||||
|
|
||||||
|
// Then: BMI = weight / (height_m^2) = 80 / (1.8 * 1.8) = 24.691...
|
||||||
|
assertThat(bmi).isWithin(EPS).of(24.691358f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun bmi_monotonicity_weightUp_heightSame_increases() {
|
||||||
|
val lib = MiScaleLib(0, 28, 165f)
|
||||||
|
val bmi1 = lib.getBMI(60f)
|
||||||
|
val bmi2 = lib.getBMI(65f)
|
||||||
|
assertThat(bmi2).isGreaterThan(bmi1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun bmi_monotonicity_heightUp_weightSame_decreases() {
|
||||||
|
val libShort = MiScaleLib(1, 35, 170f)
|
||||||
|
val libTall = MiScaleLib(1, 35, 185f)
|
||||||
|
val weight = 80f
|
||||||
|
assertThat(libTall.getBMI(weight)).isLessThan(libShort.getBMI(weight))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Regression values for full model (from your dumps) ------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun regression_male_30y_180cm_80kg_imp500() {
|
||||||
|
val lib = MiScaleLib(1, 30, 180f)
|
||||||
|
val weight = 80f
|
||||||
|
val r = Fixture(
|
||||||
|
bmi = 24.691359f,
|
||||||
|
bodyFat = 23.315107f,
|
||||||
|
bone = 3.1254203f,
|
||||||
|
lbm = 58.222496f,
|
||||||
|
musclePct = 40.977253f,
|
||||||
|
waterPct = 52.605835f,
|
||||||
|
visceralFat = 13.359997f
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(lib.getBMI(weight)).isWithin(EPS).of(r.bmi)
|
||||||
|
assertThat(lib.getBodyFat(weight, 500f)).isWithin(1e-3f).of(r.bodyFat)
|
||||||
|
assertThat(lib.getBoneMass(weight, 500f)).isWithin(1e-3f).of(r.bone)
|
||||||
|
assertThat(lib.getLBM(weight, 500f)).isWithin(1e-3f).of(r.lbm)
|
||||||
|
assertThat(lib.getMuscle(weight, 500f)).isWithin(1e-3f).of(r.musclePct)
|
||||||
|
assertThat(lib.getWater(weight, 500f)).isWithin(1e-3f).of(r.waterPct)
|
||||||
|
assertThat(lib.getVisceralFat(weight)).isWithin(1e-3f).of(r.visceralFat)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun regression_female_28y_165cm_60kg_imp520() {
|
||||||
|
val lib = MiScaleLib(0, 28, 165f)
|
||||||
|
val weight = 60f
|
||||||
|
val r = Fixture(
|
||||||
|
bmi = 22.038567f,
|
||||||
|
bodyFat = 30.361998f,
|
||||||
|
bone = 2.4865808f,
|
||||||
|
lbm = 39.29622f,
|
||||||
|
musclePct = 40.181103f,
|
||||||
|
waterPct = 49.72153f,
|
||||||
|
visceralFat = -36.555004f
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(lib.getBMI(weight)).isWithin(EPS).of(r.bmi)
|
||||||
|
assertThat(lib.getBodyFat(weight, 520f)).isWithin(1e-3f).of(r.bodyFat)
|
||||||
|
assertThat(lib.getBoneMass(weight, 520f)).isWithin(1e-3f).of(r.bone)
|
||||||
|
assertThat(lib.getLBM(weight, 520f)).isWithin(1e-3f).of(r.lbm)
|
||||||
|
assertThat(lib.getMuscle(weight, 520f)).isWithin(1e-3f).of(r.musclePct)
|
||||||
|
assertThat(lib.getWater(weight, 520f)).isWithin(1e-3f).of(r.waterPct)
|
||||||
|
assertThat(lib.getVisceralFat(weight)).isWithin(1e-3f).of(r.visceralFat)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun regression_male_45y_175cm_95kg_imp430() {
|
||||||
|
val lib = MiScaleLib(1, 45, 175f)
|
||||||
|
val weight = 95f
|
||||||
|
val r = Fixture(
|
||||||
|
bmi = 31.020409f,
|
||||||
|
bodyFat = 32.41778f,
|
||||||
|
bone = 3.2726917f,
|
||||||
|
lbm = 60.93042f,
|
||||||
|
musclePct = 36.096416f,
|
||||||
|
waterPct = 48.2537f,
|
||||||
|
visceralFat = 24.462498f
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(lib.getBMI(weight)).isWithin(EPS).of(r.bmi)
|
||||||
|
assertThat(lib.getBodyFat(weight, 430f)).isWithin(1e-3f).of(r.bodyFat)
|
||||||
|
assertThat(lib.getBoneMass(weight, 430f)).isWithin(1e-3f).of(r.bone)
|
||||||
|
assertThat(lib.getLBM(weight, 430f)).isWithin(1e-3f).of(r.lbm)
|
||||||
|
assertThat(lib.getMuscle(weight, 430f)).isWithin(1e-3f).of(r.musclePct)
|
||||||
|
assertThat(lib.getWater(weight, 430f)).isWithin(1e-3f).of(r.waterPct)
|
||||||
|
assertThat(lib.getVisceralFat(weight)).isWithin(1e-3f).of(r.visceralFat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Special paths & edge behavior --------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun muscle_fallback_whenImpedanceZero_usesLbmRatio_andIsClamped() {
|
||||||
|
// Female, impedance=0 triggers fallback path (LBM * 0.46) → % of weight → clamp 10..60
|
||||||
|
val lib = MiScaleLib(0, 52, 160f)
|
||||||
|
val weight = 48f
|
||||||
|
|
||||||
|
// Compute expected via the same path the code uses (behavioral property, not magic number)
|
||||||
|
val lbm = lib.getLBM(weight, /* impedance */ 0f)
|
||||||
|
val expectedPct = (lbm * 0.46f) / weight * 100f
|
||||||
|
val expectedClamped = expectedPct.coerceIn(10f, 60f)
|
||||||
|
|
||||||
|
val actual = lib.getMuscle(weight, /* impedance */ 0f)
|
||||||
|
assertThat(actual).isWithin(1e-3f).of(expectedClamped)
|
||||||
|
assertThat(actual).isAtLeast(10f)
|
||||||
|
assertThat(actual).isAtMost(60f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun muscle_percentage_isClampedAt60_whenExtremelyHigh() {
|
||||||
|
// Construct params that blow up SMM/weight; expect clamp to 60%
|
||||||
|
val lib = MiScaleLib(1, 20, 190f)
|
||||||
|
val clamped = lib.getMuscle(/* weight */ 40f, /* very low impedance */ 50f)
|
||||||
|
assertThat(clamped).isWithin(EPS).of(60f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun water_derivesFromBodyFat_andUsesCoeffBranch() {
|
||||||
|
// Check: water = ((100 - BF) * 0.7) * coeff, coeff = 1.02 if <50 else 0.98
|
||||||
|
val lib = MiScaleLib(0, 50, 150f)
|
||||||
|
val weight = 100f
|
||||||
|
val imp = 700f
|
||||||
|
|
||||||
|
val bf = lib.getBodyFat(weight, imp)
|
||||||
|
val raw = (100f - bf) * 0.7f
|
||||||
|
val coeff = if (raw < 50f) 1.02f else 0.98f
|
||||||
|
val expected = raw * coeff
|
||||||
|
|
||||||
|
val water = lib.getWater(weight, imp)
|
||||||
|
assertThat(water).isWithin(1e-3f).of(expected)
|
||||||
|
if (raw < 50f) {
|
||||||
|
assertThat(water).isLessThan(50f)
|
||||||
|
} else {
|
||||||
|
assertThat(water).isGreaterThan(50f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun outputs_areFinite_forTypicalInputs() {
|
||||||
|
val lib = MiScaleLib(1, 30, 180f)
|
||||||
|
val weight = 80f
|
||||||
|
val imp = 500f
|
||||||
|
|
||||||
|
val nums = listOf(
|
||||||
|
lib.getBMI(weight),
|
||||||
|
lib.getBodyFat(weight, imp),
|
||||||
|
lib.getBoneMass(weight, imp),
|
||||||
|
lib.getLBM(weight, imp),
|
||||||
|
lib.getMuscle(weight, imp),
|
||||||
|
lib.getWater(weight, imp),
|
||||||
|
lib.getVisceralFat(weight)
|
||||||
|
)
|
||||||
|
nums.forEach { v ->
|
||||||
|
assertThat(v.isNaN()).isFalse()
|
||||||
|
assertThat(v.isInfinite()).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class Fixture(
|
||||||
|
val bmi: Float,
|
||||||
|
val bodyFat: Float,
|
||||||
|
val bone: Float,
|
||||||
|
val lbm: Float,
|
||||||
|
val musclePct: Float,
|
||||||
|
val waterPct: Float,
|
||||||
|
val visceralFat: Float
|
||||||
|
)
|
||||||
|
}
|
@@ -0,0 +1,263 @@
|
|||||||
|
/*
|
||||||
|
* openScale
|
||||||
|
* Copyright (C) 2025 olie.xdev <olie.xdeveloper@googlemail.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.health.openscale.core.bluetooth.libs
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.google.common.truth.Truth.assertWithMessage
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for [OneByoneLib].
|
||||||
|
*
|
||||||
|
* Strategy
|
||||||
|
* 1) Snapshot tests with frozen numbers (from current Java impl) to guard a Kotlin port.
|
||||||
|
* 2) Property tests (monotonicity, clamping, finite outputs, boundary behavior).
|
||||||
|
*
|
||||||
|
* NOTE: We do NOT re-implement formulas here. Snapshots are the source of truth.
|
||||||
|
*/
|
||||||
|
class OneByoneLibTest {
|
||||||
|
|
||||||
|
private val EPS = 1e-3f
|
||||||
|
|
||||||
|
// ---------- Snapshots (pre-recorded from current Java implementation) ----------
|
||||||
|
|
||||||
|
private data class Snap(
|
||||||
|
val sex: Int,
|
||||||
|
val age: Int,
|
||||||
|
val h: Float,
|
||||||
|
val w: Float,
|
||||||
|
val imp: Float,
|
||||||
|
val pt: Int,
|
||||||
|
val bmi: Float,
|
||||||
|
val bf: Float,
|
||||||
|
val lbm: Float,
|
||||||
|
val muscle: Float,
|
||||||
|
val water: Float,
|
||||||
|
val bone: Float,
|
||||||
|
val vf: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
private val FIXTURES = mapOf(
|
||||||
|
"male_mid" to Snap(
|
||||||
|
sex = 1, age = 30, h = 180f, w = 80f, imp = 500f, pt = 0,
|
||||||
|
bmi = 24.691359f, bf = 23.315102f, lbm = 61.34792f, muscle = 40.97725f,
|
||||||
|
water = 52.60584f, bone = 3.030576f, vf = 10.79977f
|
||||||
|
),
|
||||||
|
"female_mid" to Snap(
|
||||||
|
sex = 0, age = 28, h = 165f, w = 60f, imp = 520f, pt = 1,
|
||||||
|
bmi = 22.038567f, bf = 25.210106f, lbm = 44.873936f, muscle = 40.181107f,
|
||||||
|
water = 51.305866f, bone = 2.3883991f, vf = 0.70499706f
|
||||||
|
),
|
||||||
|
"male_high" to Snap(
|
||||||
|
sex = 1, age = 52, h = 175f, w = 95f, imp = 430f, pt = 2,
|
||||||
|
bmi = 31.020409f, bf = 26.381027f, lbm = 69.93803f, muscle = 35.573257f,
|
||||||
|
water = 50.502613f, bone = 3.1443515f, vf = 13.163806f
|
||||||
|
),
|
||||||
|
"imp_low" to Snap(
|
||||||
|
sex = 1, age = 25, h = 178f, w = 72f, imp = 80f, pt = 0,
|
||||||
|
bmi = 22.724403f, bf = 16.04116f, lbm = 60.450363f, muscle = 230.51118f,
|
||||||
|
water = 57.595764f, bone = 3.0263696f, vf = 9.316022f
|
||||||
|
),
|
||||||
|
"imp_mid" to Snap(
|
||||||
|
sex = 0, age = 35, h = 170f, w = 68f, imp = 300f, pt = 2,
|
||||||
|
bmi = 23.529411f, bf = 25.14642f, lbm = 50.900436f, muscle = 60.656864f,
|
||||||
|
water = 51.349552f, bone = 2.650265f, vf = 2.6039982f
|
||||||
|
),
|
||||||
|
"imp_high" to Snap(
|
||||||
|
sex = 1, age = 45, h = 182f, w = 90f, imp = 1300f, pt = 1,
|
||||||
|
bmi = 27.170633f, bf = 30.914497f, lbm = 62.176952f, muscle = 17.721643f,
|
||||||
|
water = 49.32705f, bone = 2.901557f, vf = 11.179609f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun snapshots_match_expected_outputs() {
|
||||||
|
require(FIXTURES.isNotEmpty()) { "No snapshots defined." }
|
||||||
|
|
||||||
|
FIXTURES.forEach { (name, s) ->
|
||||||
|
val lib = OneByoneLib(s.sex, s.age, s.h, s.pt)
|
||||||
|
val bf = lib.getBodyFat(s.w, s.imp)
|
||||||
|
|
||||||
|
assertWithMessage("$name:bmi").that(lib.getBMI(s.w)).isWithin(EPS).of(s.bmi)
|
||||||
|
assertWithMessage("$name:bf").that(bf).isWithin(EPS).of(s.bf)
|
||||||
|
assertWithMessage("$name:lbm").that(lib.getLBM(s.w, bf)).isWithin(EPS).of(s.lbm)
|
||||||
|
assertWithMessage("$name:muscle").that(lib.getMuscle(s.w, s.imp)).isWithin(EPS).of(s.muscle)
|
||||||
|
assertWithMessage("$name:water").that(lib.getWater(bf)).isWithin(EPS).of(s.water)
|
||||||
|
assertWithMessage("$name:bone").that(lib.getBoneMass(s.w, s.imp)).isWithin(EPS).of(s.bone)
|
||||||
|
assertWithMessage("$name:vf").that(lib.getVisceralFat(s.w)).isWithin(EPS).of(s.vf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- Generic / property-based tests ----------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun bmi_monotonicity_weightUp_increases_heightConstant() {
|
||||||
|
val lib = OneByoneLib(1, 30, 180f, 0)
|
||||||
|
val w1 = 70f
|
||||||
|
val w2 = 85f
|
||||||
|
assertThat(lib.getBMI(w2)).isGreaterThan(lib.getBMI(w1))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun bmi_monotonicity_heightUp_decreases_weightConstant() {
|
||||||
|
val libShort = OneByoneLib(1, 30, 170f, 0)
|
||||||
|
val libTall = OneByoneLib(1, 30, 190f, 0)
|
||||||
|
val w = 80f
|
||||||
|
assertThat(libTall.getBMI(w)).isLessThan(libShort.getBMI(w))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun water_switch_coeff_below_and_above_50() {
|
||||||
|
val lib = OneByoneLib(0, 40, 165f, 1)
|
||||||
|
val bfHigh = 35f // → (100-35)*0.7 = 45.5 < 50 → *1.02
|
||||||
|
val bfLow = 20f // → (100-20)*0.7 = 56 > 50 → *0.98
|
||||||
|
val wHigh = lib.getWater(bfHigh)
|
||||||
|
val wLow = lib.getWater(bfLow)
|
||||||
|
assertThat(wHigh).isLessThan(50f)
|
||||||
|
assertThat(wLow).isGreaterThan(50f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun boneMass_isReasonablyClamped_between_0_5_and_8_0() {
|
||||||
|
val lib = OneByoneLib(0, 55, 170f, 2)
|
||||||
|
// Explore some extreme ranges
|
||||||
|
val candidates = listOf(
|
||||||
|
40f to 1400f,
|
||||||
|
150f to 200f,
|
||||||
|
55f to 600f,
|
||||||
|
95f to 300f,
|
||||||
|
)
|
||||||
|
candidates.forEach { (w, imp) ->
|
||||||
|
val bone = lib.getBoneMass(w, imp)
|
||||||
|
assertThat(bone).isAtLeast(0.5f)
|
||||||
|
assertThat(bone).isAtMost(8.0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun muscle_reacts_to_impedance_reasonably() {
|
||||||
|
val sex = 1; val age = 30; val h = 180f; val w = 80f
|
||||||
|
val lib = OneByoneLib(sex, age, h, 0)
|
||||||
|
|
||||||
|
val impHigh = 1300f
|
||||||
|
val impMid = 400f
|
||||||
|
val impLow = 80f
|
||||||
|
|
||||||
|
val mHigh = lib.getMuscle(w, impHigh)
|
||||||
|
val mMid = lib.getMuscle(w, impMid)
|
||||||
|
val mLow = lib.getMuscle(w, impLow)
|
||||||
|
|
||||||
|
// Lower impedance tends to increase SMM estimate (classic BIA behavior)
|
||||||
|
assertThat(mLow).isGreaterThan(mMid)
|
||||||
|
assertThat(mMid).isGreaterThan(mHigh)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun bodyFat_stays_within_reasonable_bounds() {
|
||||||
|
val lib = OneByoneLib(1, 35, 180f, 1)
|
||||||
|
val weights = listOf(50f, 70f, 90f, 110f)
|
||||||
|
val imps = listOf(80f, 300f, 600f, 1200f)
|
||||||
|
for (w in weights) for (imp in imps) {
|
||||||
|
val bf = lib.getBodyFat(w, imp)
|
||||||
|
// Implementation clamps to [1, 45]; allow small epsilon
|
||||||
|
assertThat(bf).isAtLeast(1f - 1e-3f)
|
||||||
|
assertThat(bf).isAtMost(45f + 1e-3f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun peopleType_influences_outputs() {
|
||||||
|
val base = OneByoneLib(1, 40, 175f, 0)
|
||||||
|
val mid = OneByoneLib(1, 40, 175f, 1)
|
||||||
|
val high = OneByoneLib(1, 40, 175f, 2)
|
||||||
|
val w = 85f; val imp = 450f
|
||||||
|
|
||||||
|
val boneBase = base.getBoneMass(w, imp)
|
||||||
|
val boneMid = mid.getBoneMass(w, imp)
|
||||||
|
val boneHigh = high.getBoneMass(w, imp)
|
||||||
|
|
||||||
|
// Different activity types should yield distinct (but not crazy) values.
|
||||||
|
assertThat(abs(boneBase - boneMid)).isGreaterThan(0.0f)
|
||||||
|
assertThat(abs(boneMid - boneHigh)).isGreaterThan(0.0f)
|
||||||
|
|
||||||
|
// Guard against wild divergence
|
||||||
|
val minV = min(boneBase, min(boneMid, boneHigh))
|
||||||
|
val maxV = max(boneBase, max(boneMid, boneHigh))
|
||||||
|
assertThat(maxV - minV).isLessThan(2.0f) // heuristic guard
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sex_flag_affects_outputs() {
|
||||||
|
val male = OneByoneLib(1, 32, 178f, 1)
|
||||||
|
val female = OneByoneLib(0, 32, 178f, 1)
|
||||||
|
val w = 75f; val imp = 420f
|
||||||
|
|
||||||
|
val bfM = male.getBodyFat(w, imp)
|
||||||
|
val bfF = female.getBodyFat(w, imp)
|
||||||
|
|
||||||
|
// Expect some difference between sexes
|
||||||
|
assertThat(abs(bfM - bfF)).isGreaterThan(0.1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun outputs_are_finite_for_typical_ranges() {
|
||||||
|
val lib = OneByoneLib(1, 30, 180f, 0)
|
||||||
|
val w = 80f; val imp = 500f
|
||||||
|
val bf = lib.getBodyFat(w, imp)
|
||||||
|
val values = listOf(
|
||||||
|
lib.getBMI(w),
|
||||||
|
bf,
|
||||||
|
lib.getLBM(w, bf),
|
||||||
|
lib.getMuscle(w, imp),
|
||||||
|
lib.getWater(bf),
|
||||||
|
lib.getBoneMass(w, imp),
|
||||||
|
lib.getVisceralFat(w)
|
||||||
|
)
|
||||||
|
values.forEach {
|
||||||
|
assertThat(it.isNaN()).isFalse()
|
||||||
|
assertThat(it.isInfinite()).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Helper to (re)generate snapshot values if formulas change ----------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun print_current_outputs_for_fixtures() {
|
||||||
|
fun dump(sex: Int, age: Int, h: Float, w: Float, imp: Float, pt: Int) {
|
||||||
|
val lib = OneByoneLib(sex, age, h, pt)
|
||||||
|
val bf = lib.getBodyFat(w, imp)
|
||||||
|
println(
|
||||||
|
"SNAP -> sex=$sex age=$age h=$h w=$w imp=$imp pt=$pt | " +
|
||||||
|
"bmi=${lib.getBMI(w)}; bf=$bf; " +
|
||||||
|
"lbm=${lib.getLBM(w, bf)}; muscle=${lib.getMuscle(w, imp)}; " +
|
||||||
|
"water=${lib.getWater(bf)}; bone=${lib.getBoneMass(w, imp)}; vf=${lib.getVisceralFat(w)}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-run if you intentionally modify formulas; then paste outputs into FIXTURES above.
|
||||||
|
dump(1, 30, 180f, 80f, 500f, 0)
|
||||||
|
dump(0, 28, 165f, 60f, 520f, 1)
|
||||||
|
dump(1, 52, 175f, 95f, 430f, 2)
|
||||||
|
dump(1, 25, 178f, 72f, 80f, 0)
|
||||||
|
dump(0, 35, 170f, 68f, 300f, 2)
|
||||||
|
dump(1, 45, 182f, 90f, 1300f, 1)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,298 @@
|
|||||||
|
/*
|
||||||
|
* openScale
|
||||||
|
* Copyright (C) 2025 olie.xdev <olie.xdeveloper@googlemail.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.health.openscale.core.bluetooth.libs
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.google.common.truth.Truth.assertWithMessage
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for [OneByoneNewLib].
|
||||||
|
*
|
||||||
|
* - Snapshot tests (FIXTURES) sichern, dass sich die Outputs nach Refactor/Port nicht verändern.
|
||||||
|
* - Property-Tests prüfen Monotonie, Clamping, Finite-Werte etc.
|
||||||
|
*/
|
||||||
|
class OneByoneNewLibTest {
|
||||||
|
|
||||||
|
private val EPS = 1e-3f
|
||||||
|
|
||||||
|
// ---------- Snapshots (aus deiner Ausgabe eingefügt) ----------
|
||||||
|
|
||||||
|
private data class Snap(
|
||||||
|
val sex: Int,
|
||||||
|
val age: Int,
|
||||||
|
val h: Float,
|
||||||
|
val w: Float,
|
||||||
|
val imp: Int,
|
||||||
|
val pt: Int,
|
||||||
|
val bmi: Float,
|
||||||
|
val lbm: Float,
|
||||||
|
val bmmrCoeff: Float,
|
||||||
|
val bmmr: Float,
|
||||||
|
val bodyFatPct: Float,
|
||||||
|
val boneMass: Float,
|
||||||
|
val muscleMass: Float,
|
||||||
|
val skelMusclePct: Float,
|
||||||
|
val visceralFat: Float,
|
||||||
|
val waterPct: Float,
|
||||||
|
val proteinPct: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
private val FIXTURES: Map<String, Snap> = mapOf(
|
||||||
|
"male_mid" to Snap(
|
||||||
|
sex = 1, age = 30, h = 180f, w = 80f, imp = 500, pt = 0,
|
||||||
|
bmi = 24.691359f, lbm = 62.14792f, bmmrCoeff = 21.0f, bmmr = 1000.0f,
|
||||||
|
bodyFatPct = 23.315102f, boneMass = 3.1254208f, muscleMass = 58.2225f,
|
||||||
|
skelMusclePct = 40.566765f, visceralFat = 10.79977f, waterPct = 52.60584f,
|
||||||
|
proteinPct = 15.963814f
|
||||||
|
),
|
||||||
|
"female_mid" to Snap(
|
||||||
|
sex = 0, age = 28, h = 165f, w = 60f, imp = 520, pt = 1,
|
||||||
|
bmi = 22.038567f, lbm = 51.032806f, bmmrCoeff = 22.0f, bmmr = 1000.0f,
|
||||||
|
bodyFatPct = 28.27285f, boneMass = 2.486581f, muscleMass = 40.549713f,
|
||||||
|
skelMusclePct = 36.45647f, visceralFat = 4.704997f, waterPct = 49.204823f,
|
||||||
|
proteinPct = 14.44164f
|
||||||
|
),
|
||||||
|
"imp_low" to Snap(
|
||||||
|
sex = 1, age = 25, h = 178f, w = 72f, imp = 80, pt = 0,
|
||||||
|
bmi = 22.724403f, lbm = 62.06637f, bmmrCoeff = 23.0f, bmmr = 1000.0f,
|
||||||
|
bodyFatPct = 14.907819f, boneMass = 3.1212144f, muscleMass = 58.145157f,
|
||||||
|
skelMusclePct = 45.008743f, visceralFat = 9.316022f, waterPct = 58.373234f,
|
||||||
|
proteinPct = 17.714064f
|
||||||
|
),
|
||||||
|
"imp_mid" to Snap(
|
||||||
|
sex = 0, age = 35, h = 170f, w = 68f, imp = 300, pt = 2,
|
||||||
|
bmi = 23.529411f, lbm = 56.22662f, bmmrCoeff = 20.0f, bmmr = 1000.0f,
|
||||||
|
bodyFatPct = 31.690466f, boneMass = 2.754478f, muscleMass = 43.696007f,
|
||||||
|
skelMusclePct = 36.679123f, visceralFat = 6.603998f, waterPct = 48.773006f,
|
||||||
|
proteinPct = 11.583979f
|
||||||
|
),
|
||||||
|
"imp_high" to Snap(
|
||||||
|
sex = 1, age = 45, h = 182f, w = 90f, imp = 1300, pt = 1,
|
||||||
|
bmi = 27.170633f, lbm = 59.750725f, bmmrCoeff = 21.0f, bmmr = 1000.0f,
|
||||||
|
bodyFatPct = 34.49919f, boneMass = 3.0017734f, muscleMass = 55.948956f,
|
||||||
|
skelMusclePct = 36.065098f, visceralFat = 13.974511f, waterPct = 46.76758f,
|
||||||
|
proteinPct = 11.656517f
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun snapshots_match_expected_outputs() {
|
||||||
|
FIXTURES.forEach { (name, s) ->
|
||||||
|
val lib = OneByoneNewLib(s.sex, s.age, s.h, s.pt)
|
||||||
|
|
||||||
|
val bmi = lib.getBMI(s.w)
|
||||||
|
val lbm = lib.getLBM(s.w, s.imp)
|
||||||
|
val coeff = lib.getBMMRCoeff(s.w)
|
||||||
|
val bmmr = lib.getBMMR(s.w)
|
||||||
|
val bf = lib.getBodyFatPercentage(s.w, s.imp)
|
||||||
|
val bone = lib.getBoneMass(s.w, s.imp)
|
||||||
|
val mm = lib.getMuscleMass(s.w, s.imp)
|
||||||
|
val skm = lib.getSkeletonMusclePercentage(s.w, s.imp)
|
||||||
|
val vf = lib.getVisceralFat(s.w)
|
||||||
|
val water = lib.getWaterPercentage(s.w, s.imp)
|
||||||
|
val prot = lib.getProteinPercentage(s.w, s.imp)
|
||||||
|
|
||||||
|
assertWithMessage("$name:bmi").that(bmi).isWithin(EPS).of(s.bmi)
|
||||||
|
assertWithMessage("$name:lbm").that(lbm).isWithin(EPS).of(s.lbm)
|
||||||
|
assertWithMessage("$name:bmmrCoeff").that(coeff).isWithin(EPS).of(s.bmmrCoeff)
|
||||||
|
assertWithMessage("$name:bmmr").that(bmmr).isWithin(EPS).of(s.bmmr)
|
||||||
|
assertWithMessage("$name:bf%").that(bf).isWithin(EPS).of(s.bodyFatPct)
|
||||||
|
assertWithMessage("$name:bone").that(bone).isWithin(EPS).of(s.boneMass)
|
||||||
|
assertWithMessage("$name:muscleMass").that(mm).isWithin(EPS).of(s.muscleMass)
|
||||||
|
assertWithMessage("$name:skelMuscle%").that(skm).isWithin(EPS).of(s.skelMusclePct)
|
||||||
|
assertWithMessage("$name:visceralFat").that(vf).isWithin(EPS).of(s.visceralFat)
|
||||||
|
assertWithMessage("$name:water%").that(water).isWithin(EPS).of(s.waterPct)
|
||||||
|
assertWithMessage("$name:protein%").that(prot).isWithin(EPS).of(s.proteinPct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Behavior / Property tests ----------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun bmi_is_bounded_and_monotonic_with_weight() {
|
||||||
|
val lib = OneByoneNewLib(1, 30, 180f, 0)
|
||||||
|
val b1 = lib.getBMI(60f)
|
||||||
|
val b2 = lib.getBMI(80f)
|
||||||
|
val b3 = lib.getBMI(100f)
|
||||||
|
|
||||||
|
listOf(b1, b2, b3).forEach {
|
||||||
|
assertThat(it).isAtLeast(10f - EPS)
|
||||||
|
assertThat(it).isAtMost(90f + EPS)
|
||||||
|
}
|
||||||
|
assertThat(b2).isGreaterThan(b1)
|
||||||
|
assertThat(b3).isGreaterThan(b2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun lbm_varies_with_impedance_and_weight() {
|
||||||
|
val lib = OneByoneNewLib(0, 28, 165f, 1)
|
||||||
|
val w = 60f
|
||||||
|
|
||||||
|
val lbmHighImp = lib.getLBM(w, 1200)
|
||||||
|
val lbmLowImp = lib.getLBM(w, 200)
|
||||||
|
assertThat(lbmLowImp).isGreaterThan(lbmHighImp)
|
||||||
|
|
||||||
|
val lbmHeavier = lib.getLBM(75f, 300)
|
||||||
|
val lbmLighter = lib.getLBM(55f, 300)
|
||||||
|
assertThat(lbmHeavier).isGreaterThan(lbmLighter)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun bmmrCoeff_follows_age_bands_and_sex() {
|
||||||
|
fun coeff(sex:Int, age:Int): Float =
|
||||||
|
OneByoneNewLib(sex, age, 170f, 0).getBMMRCoeff(70f)
|
||||||
|
|
||||||
|
// male bands
|
||||||
|
assertThat(coeff(1, 10)).isWithin(EPS).of(36f)
|
||||||
|
assertThat(coeff(1, 14)).isWithin(EPS).of(30f)
|
||||||
|
assertThat(coeff(1, 17)).isWithin(EPS).of(26f)
|
||||||
|
assertThat(coeff(1, 25)).isWithin(EPS).of(23f)
|
||||||
|
assertThat(coeff(1, 50)).isWithin(EPS).of(20f)
|
||||||
|
|
||||||
|
// female bands
|
||||||
|
assertThat(coeff(0, 10)).isWithin(EPS).of(34f)
|
||||||
|
assertThat(coeff(0, 15)).isWithin(EPS).of(29f)
|
||||||
|
assertThat(coeff(0, 17)).isWithin(EPS).of(24f)
|
||||||
|
assertThat(coeff(0, 25)).isWithin(EPS).of(22f)
|
||||||
|
assertThat(coeff(0, 50)).isWithin(EPS).of(19f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun bmmr_is_bounded_and_differs_by_sex() {
|
||||||
|
val h = 180f
|
||||||
|
val age = 30
|
||||||
|
|
||||||
|
val male = OneByoneNewLib(1, age, h, 0).getBMMR(22f) // ~806 (ungeclamped)
|
||||||
|
val female = OneByoneNewLib(0, age, h, 0).getBMMR(19f) // ~802 (ungeclamped)
|
||||||
|
|
||||||
|
assertThat(male).isAtLeast(500f - 1e-3f)
|
||||||
|
assertThat(male).isAtMost(1000f + 1e-3f)
|
||||||
|
assertThat(female).isAtLeast(500f - 1e-3f)
|
||||||
|
assertThat(female).isAtMost(1000f + 1e-3f)
|
||||||
|
|
||||||
|
assertThat(abs(male - female)).isGreaterThan(0.1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun water_switches_coeff_around_50_and_is_bounded() {
|
||||||
|
val lib = OneByoneNewLib(1, 35, 175f, 1)
|
||||||
|
val w = 80f
|
||||||
|
val waterLow = lib.getWaterPercentage(w, 1200) // typ. <50
|
||||||
|
val waterHigh = lib.getWaterPercentage(w, 200) // typ. >50
|
||||||
|
|
||||||
|
listOf(waterLow, waterHigh).forEach {
|
||||||
|
assertThat(it).isAtLeast(35f - EPS)
|
||||||
|
assertThat(it).isAtMost(75f + EPS)
|
||||||
|
}
|
||||||
|
// lose heuristics; just ensure they are not identical and near the “switch” region across scenarios
|
||||||
|
assertThat(abs(waterHigh - waterLow)).isGreaterThan(0.5f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun boneMass_is_bounded_and_varies_with_impedance() {
|
||||||
|
val lib = OneByoneNewLib(0, 50, 168f, 0)
|
||||||
|
val w = 70f
|
||||||
|
|
||||||
|
val boneHighImp = lib.getBoneMass(w, 1200)
|
||||||
|
val boneLowImp = lib.getBoneMass(w, 200)
|
||||||
|
|
||||||
|
listOf(boneHighImp, boneLowImp).forEach {
|
||||||
|
assertThat(it).isAtLeast(0.5f - 1e-3f)
|
||||||
|
assertThat(it).isAtMost(8.0f + 1e-3f)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(abs(boneLowImp - boneHighImp)).isGreaterThan(0.0f)
|
||||||
|
assertThat(boneLowImp).isGreaterThan(boneHighImp)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun muscleMass_is_bounded_and_correlates_with_impedance() {
|
||||||
|
val lib = OneByoneNewLib(1, 28, 180f, 0)
|
||||||
|
val w = 82f
|
||||||
|
|
||||||
|
val mmHighImp = lib.getMuscleMass(w, 1200)
|
||||||
|
val mmLowImp = lib.getMuscleMass(w, 200)
|
||||||
|
|
||||||
|
listOf(mmHighImp, mmLowImp).forEach {
|
||||||
|
assertThat(it).isAtLeast(10f - EPS)
|
||||||
|
assertThat(it).isAtMost(120f + EPS)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(mmLowImp).isGreaterThan(mmHighImp)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun skeletonMuscle_is_finite_and_reasonable_range() {
|
||||||
|
val lib = OneByoneNewLib(0, 33, 165f, 2)
|
||||||
|
val sm = lib.getSkeletonMusclePercentage(58f, 400)
|
||||||
|
|
||||||
|
assertThat(sm.isNaN()).isFalse()
|
||||||
|
assertThat(sm.isInfinite()).isFalse()
|
||||||
|
assertThat(sm).isGreaterThan(-20f)
|
||||||
|
assertThat(sm).isLessThan(120f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun bodyFat_and_protein_are_finite() {
|
||||||
|
val lib = OneByoneNewLib(1, 40, 182f, 1)
|
||||||
|
val w = 90f
|
||||||
|
val imp = 500
|
||||||
|
val bf = lib.getBodyFatPercentage(w, imp)
|
||||||
|
val prot = lib.getProteinPercentage(w, imp)
|
||||||
|
|
||||||
|
assertThat(bf.isNaN()).isFalse()
|
||||||
|
assertThat(prot.isNaN()).isFalse()
|
||||||
|
assertThat(bf.isInfinite()).isFalse()
|
||||||
|
assertThat(prot.isInfinite()).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Helper: re-print current values if du neue Fixtures brauchst ----------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun print_current_outputs_for_fixtures() {
|
||||||
|
fun dump(tag:String, sex:Int, age:Int, h:Float, w:Float, imp:Int, pt:Int) {
|
||||||
|
val lib = OneByoneNewLib(sex, age, h, pt)
|
||||||
|
val bmi = lib.getBMI(w)
|
||||||
|
val lbm = lib.getLBM(w, imp)
|
||||||
|
val coeff = lib.getBMMRCoeff(w)
|
||||||
|
val bmmr = lib.getBMMR(w)
|
||||||
|
val bf = lib.getBodyFatPercentage(w, imp)
|
||||||
|
val bone = lib.getBoneMass(w, imp)
|
||||||
|
val mm = lib.getMuscleMass(w, imp)
|
||||||
|
val skm = lib.getSkeletonMusclePercentage(w, imp)
|
||||||
|
val vf = lib.getVisceralFat(w)
|
||||||
|
val water = lib.getWaterPercentage(w, imp)
|
||||||
|
val prot = lib.getProteinPercentage(w, imp)
|
||||||
|
|
||||||
|
println(
|
||||||
|
"SNAP $tag -> sex=$sex age=$age h=$h w=$w imp=$imp pt=$pt | " +
|
||||||
|
"bmi=$bmi; lbm=$lbm; bmmrCoeff=$coeff; bmmr=$bmmr; " +
|
||||||
|
"bf%=$bf; bone=$bone; muscleMass=$mm; skelMuscle%=$skm; " +
|
||||||
|
"visceralFat=$vf; water%=$water; protein%=$prot"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
dump("male_mid", 1, 30, 180f, 80f, 500, 0)
|
||||||
|
dump("female_mid", 0, 28, 165f, 60f, 520, 1)
|
||||||
|
dump("imp_low", 1, 25, 178f, 72f, 80, 0)
|
||||||
|
dump("imp_mid", 0, 35, 170f, 68f, 300, 2)
|
||||||
|
dump("imp_high", 1, 45, 182f, 90f,1300, 1)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* openScale
|
||||||
|
* Copyright (C) 2025 olie.xdev <olie.xdeveloper@googlemail.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.health.openscale.core.bluetooth.libs
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertWithMessage
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
class SoehnleLibTest {
|
||||||
|
|
||||||
|
private val EPS = 1e-3f
|
||||||
|
|
||||||
|
// -------- Snapshot-Struktur --------
|
||||||
|
private data class Snap(
|
||||||
|
val isMale: Boolean,
|
||||||
|
val age: Int,
|
||||||
|
val h: Float, // height (cm)
|
||||||
|
val w: Float, // weight (kg)
|
||||||
|
val imp50: Float, // 50 kHz impedance
|
||||||
|
val imp5: Float, // 5 kHz impedance
|
||||||
|
val activity: Int, // activityLevel (1..5)
|
||||||
|
val bmi: Float,
|
||||||
|
val fat: Float,
|
||||||
|
val water: Float,
|
||||||
|
val muscle: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
// -------- Fixe Snapshots (deine Werte) --------
|
||||||
|
private val FIXTURES: Map<String, Snap> = mapOf(
|
||||||
|
"male_mid" to Snap(
|
||||||
|
isMale = true, age = 30, h = 180f, w = 80f,
|
||||||
|
imp50 = 500f, imp5 = 200f, activity = 3,
|
||||||
|
bmi = 24.691359f, fat = 18.604935f, water = 54.8644f, muscle = 9.49897f
|
||||||
|
),
|
||||||
|
"female_mid" to Snap(
|
||||||
|
isMale = false, age = 28, h = 165f, w = 60f,
|
||||||
|
imp50 = 520f, imp5 = 210f, activity = 4,
|
||||||
|
bmi = 22.038567f, fat = 26.137234f, water = 56.005848f, muscle = 5.708269f
|
||||||
|
),
|
||||||
|
"male_active5" to Snap(
|
||||||
|
isMale = true, age = 35, h = 178f, w = 85f,
|
||||||
|
imp50 = 480f, imp5 = 190f, activity = 5,
|
||||||
|
bmi = 26.827421f, fat = 26.860249f, water = 55.484665f, muscle = 10.4964695f
|
||||||
|
),
|
||||||
|
"female_low" to Snap(
|
||||||
|
isMale = false, age = 45, h = 160f, w = 70f,
|
||||||
|
imp50 = 700f, imp5 = 250f, activity = 1,
|
||||||
|
bmi = 27.34375f, fat = 48.433907f, water = 38.981922f, muscle = 2.8784876f
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// -------- Snapshot-Check --------
|
||||||
|
@Test
|
||||||
|
fun snapshots_match_expected_outputs() {
|
||||||
|
require(FIXTURES.isNotEmpty()) { "No snapshots defined." }
|
||||||
|
|
||||||
|
FIXTURES.forEach { (name, s) ->
|
||||||
|
val lib = SoehnleLib(s.isMale, s.age, s.h, s.activity)
|
||||||
|
|
||||||
|
assertWithMessage("$name:bmi")
|
||||||
|
.that(lib.computeBodyMassIndex(s.w))
|
||||||
|
.isWithin(EPS).of(s.bmi)
|
||||||
|
|
||||||
|
assertWithMessage("$name:fat%")
|
||||||
|
.that(lib.getFat(s.w, s.imp50))
|
||||||
|
.isWithin(EPS).of(s.fat)
|
||||||
|
|
||||||
|
assertWithMessage("$name:water%")
|
||||||
|
.that(lib.getWater(s.w, s.imp50))
|
||||||
|
.isWithin(EPS).of(s.water)
|
||||||
|
|
||||||
|
assertWithMessage("$name:muscle%")
|
||||||
|
.that(lib.getMuscle(s.w, s.imp50, s.imp5))
|
||||||
|
.isWithin(EPS).of(s.muscle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- Property-Tests --------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun bmi_monotonic_with_weight() {
|
||||||
|
val lib = SoehnleLib(true, 30, 180f, 3)
|
||||||
|
val bmi1 = lib.computeBodyMassIndex(70f)
|
||||||
|
val bmi2 = lib.computeBodyMassIndex(85f)
|
||||||
|
assertWithMessage("BMI should increase when weight increases")
|
||||||
|
.that(bmi2).isGreaterThan(bmi1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fat_increases_with_impedance50() {
|
||||||
|
val lib = SoehnleLib(true, 35, 178f, 3)
|
||||||
|
val w = 82f
|
||||||
|
val low = lib.getFat(w, 300f)
|
||||||
|
val high = lib.getFat(w, 600f)
|
||||||
|
assertWithMessage("Fat% should increase with imp50 for same person/weight")
|
||||||
|
.that(high).isGreaterThan(low)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun water_in_reasonable_range() {
|
||||||
|
val lib = SoehnleLib( false, 29, 165f, 4)
|
||||||
|
val water = lib.getWater(60f, 520f)
|
||||||
|
assertInRange("Water%", water, 30f, 75f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun muscle_in_reasonable_range() {
|
||||||
|
val lib = SoehnleLib( true, 40, 182f, 5)
|
||||||
|
val muscle = lib.getMuscle(90f, 500f, 220f)
|
||||||
|
assertInRange("Muscle%", muscle, 0f, 70f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun male_vs_female_fat_differs_same_inputs() {
|
||||||
|
val age = 30; val h = 178f; val act = 3; val w = 75f; val imp50 = 500f
|
||||||
|
val m = SoehnleLib(true, age, h, act).getFat(w, imp50)
|
||||||
|
val f = SoehnleLib(false, age, h, act).getFat(w, imp50)
|
||||||
|
assertWithMessage("Male vs Female fat% should differ")
|
||||||
|
.that(abs(m - f)).isGreaterThan(0.1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- Helper --------
|
||||||
|
private fun assertInRange(label: String, value: Float, lo: Float, hi: Float) {
|
||||||
|
assertWithMessage("$label lower bound ($lo)")
|
||||||
|
.that(value).isAtLeast(lo)
|
||||||
|
assertWithMessage("$label upper bound ($hi)")
|
||||||
|
.that(value).isAtMost(hi)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,256 @@
|
|||||||
|
/*
|
||||||
|
* openScale
|
||||||
|
* Copyright (C) 2025 olie.xdev <olie.xdeveloper@googlemail.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.health.openscale.core.bluetooth.libs
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for [TrisaBodyAnalyzeLib].
|
||||||
|
*
|
||||||
|
* - Regression fixtures use outputs computed from the current formulas
|
||||||
|
* to guard against accidental changes.
|
||||||
|
* - Behavioral tests verify key monotonicity/branch properties.
|
||||||
|
*/
|
||||||
|
class TrisaBodyAnalyzeLibTest {
|
||||||
|
|
||||||
|
private val EPS = 1e-3f // general float tolerance
|
||||||
|
|
||||||
|
// --- Simple BMI checks ----------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun bmi_isComputedCorrectly_forTypicalMale() {
|
||||||
|
val lib = TrisaBodyAnalyzeLib(1, 30, 180f)
|
||||||
|
val weight = 80f
|
||||||
|
|
||||||
|
val bmi = lib.getBMI(weight)
|
||||||
|
|
||||||
|
assertThat(bmi).isWithin(EPS).of(24.691358f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun bmi_monotonicity_weightUp_heightSame_increases() {
|
||||||
|
val lib = TrisaBodyAnalyzeLib(0, 28, 165f)
|
||||||
|
val bmi1 = lib.getBMI(60f)
|
||||||
|
val bmi2 = lib.getBMI(65f)
|
||||||
|
assertThat(bmi2).isGreaterThan(bmi1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun bmi_monotonicity_heightUp_weightSame_decreases() {
|
||||||
|
val shorty = TrisaBodyAnalyzeLib(1, 35, 170f)
|
||||||
|
val tall = TrisaBodyAnalyzeLib(1, 35, 185f)
|
||||||
|
val weight = 80f
|
||||||
|
assertThat(tall.getBMI(weight)).isLessThan(shorty.getBMI(weight))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Behavioral properties -----------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun impedance_effects_haveExpectedDirections() {
|
||||||
|
val male = TrisaBodyAnalyzeLib(1, 30, 180f)
|
||||||
|
val female = TrisaBodyAnalyzeLib(0, 30, 165f)
|
||||||
|
|
||||||
|
val w = 70f
|
||||||
|
val impLow = 300f
|
||||||
|
val impHigh = 700f
|
||||||
|
|
||||||
|
assertThat(male.getWater(w, impHigh)).isLessThan(male.getWater(w, impLow))
|
||||||
|
assertThat(male.getMuscle(w, impHigh)).isLessThan(male.getMuscle(w, impLow))
|
||||||
|
assertThat(male.getBone(w, impHigh)).isLessThan(male.getBone(w, impLow))
|
||||||
|
assertThat(male.getFat(w, impHigh)).isGreaterThan(male.getFat(w, impLow))
|
||||||
|
|
||||||
|
assertThat(female.getWater(w, impHigh)).isLessThan(female.getWater(w, impLow))
|
||||||
|
assertThat(female.getMuscle(w, impHigh)).isLessThan(female.getMuscle(w, impLow))
|
||||||
|
assertThat(female.getBone(w, impHigh)).isLessThan(female.getBone(w, impLow))
|
||||||
|
assertThat(female.getFat(w, impHigh)).isGreaterThan(female.getFat(w, impLow))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sex_flag_changes_branch_outputs() {
|
||||||
|
val male = TrisaBodyAnalyzeLib(1, 30, 175f)
|
||||||
|
val female = TrisaBodyAnalyzeLib(0, 30, 175f)
|
||||||
|
val w = 70f
|
||||||
|
val imp = 500f
|
||||||
|
|
||||||
|
assertThat(male.getWater(w, imp)).isNotEqualTo(female.getWater(w, imp))
|
||||||
|
assertThat(male.getFat(w, imp)).isNotEqualTo(female.getFat(w, imp))
|
||||||
|
assertThat(male.getMuscle(w, imp)).isNotEqualTo(female.getMuscle(w, imp))
|
||||||
|
assertThat(male.getBone(w, imp)).isNotEqualTo(female.getBone(w, imp))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun outputs_areFinite_forTypicalInputs() {
|
||||||
|
val lib = TrisaBodyAnalyzeLib(1, 30, 180f)
|
||||||
|
val w = 80f
|
||||||
|
val imp = 500f
|
||||||
|
|
||||||
|
val nums = listOf(
|
||||||
|
lib.getBMI(w),
|
||||||
|
lib.getWater(w, imp),
|
||||||
|
lib.getFat(w, imp),
|
||||||
|
lib.getMuscle(w, imp),
|
||||||
|
lib.getBone(w, imp)
|
||||||
|
)
|
||||||
|
|
||||||
|
nums.forEach { v ->
|
||||||
|
assertThat(v.isNaN()).isFalse()
|
||||||
|
assertThat(v.isInfinite()).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Regression fixtures -------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun regression_male_30y_180cm_80kg_imp500() {
|
||||||
|
val lib = TrisaBodyAnalyzeLib(1, 30, 180f)
|
||||||
|
val w = 80f
|
||||||
|
val imp = 500f
|
||||||
|
val r = Fixture(
|
||||||
|
bmi = 24.691359f,
|
||||||
|
water = 57.031845f,
|
||||||
|
fat = 23.186619f,
|
||||||
|
muscle = 40.767307f,
|
||||||
|
bone = 4.254889f
|
||||||
|
)
|
||||||
|
checkFixture(lib, w, imp, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun regression_female_28y_165cm_60kg_imp520() {
|
||||||
|
val lib = TrisaBodyAnalyzeLib(0, 28, 165f)
|
||||||
|
val w = 60f
|
||||||
|
val imp = 520f
|
||||||
|
val r = Fixture(
|
||||||
|
bmi = 22.038567f,
|
||||||
|
water = 51.246567f,
|
||||||
|
fat = 27.63467f,
|
||||||
|
muscle = 32.776436f,
|
||||||
|
bone = 4.575968f
|
||||||
|
)
|
||||||
|
checkFixture(lib, w, imp, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun regression_male_45y_175cm_95kg_imp430() {
|
||||||
|
val lib = TrisaBodyAnalyzeLib(1, 45, 175f)
|
||||||
|
val w = 95f
|
||||||
|
val imp = 430f
|
||||||
|
val r = Fixture(
|
||||||
|
bmi = 31.020409f,
|
||||||
|
water = 51.385693f,
|
||||||
|
fat = 34.484245f,
|
||||||
|
muscle = 30.524948f,
|
||||||
|
bone = 3.1716952f
|
||||||
|
)
|
||||||
|
checkFixture(lib, w, imp, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun regression_female_55y_160cm_50kg_imp600() {
|
||||||
|
val lib = TrisaBodyAnalyzeLib(0, 55, 160f)
|
||||||
|
val w = 50f
|
||||||
|
val imp = 600f
|
||||||
|
val r = Fixture(
|
||||||
|
bmi = 19.53125f,
|
||||||
|
water = 55.407524f,
|
||||||
|
fat = 26.659752f,
|
||||||
|
muscle = 27.356312f,
|
||||||
|
bone = 3.8092093f
|
||||||
|
)
|
||||||
|
checkFixture(lib, w, imp, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun regression_male_20y_190cm_65kg_imp480() {
|
||||||
|
val lib = TrisaBodyAnalyzeLib(1, 20, 190f)
|
||||||
|
val w = 65f
|
||||||
|
val imp = 480f
|
||||||
|
val r = Fixture(
|
||||||
|
bmi = 18.00554f,
|
||||||
|
water = 64.203964f,
|
||||||
|
fat = 10.668964f,
|
||||||
|
muscle = 49.972504f,
|
||||||
|
bone = 5.2273664f
|
||||||
|
)
|
||||||
|
checkFixture(lib, w, imp, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun regression_female_22y_155cm_55kg_imp510() {
|
||||||
|
val lib = TrisaBodyAnalyzeLib(0, 22, 155f)
|
||||||
|
val w = 55f
|
||||||
|
val imp = 510f
|
||||||
|
val r = Fixture(
|
||||||
|
bmi = 22.89282f,
|
||||||
|
water = 49.936302f,
|
||||||
|
fat = 28.405312f,
|
||||||
|
muscle = 33.747982f,
|
||||||
|
bone = 4.713689f
|
||||||
|
)
|
||||||
|
checkFixture(lib, w, imp, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun regression_male_35y_175cm_85kg_imp200() {
|
||||||
|
val lib = TrisaBodyAnalyzeLib(1, 35, 175f)
|
||||||
|
val w = 85f
|
||||||
|
val imp = 200f
|
||||||
|
val r = Fixture(
|
||||||
|
bmi = 27.755102f,
|
||||||
|
water = 56.290474f,
|
||||||
|
fat = 25.228241f,
|
||||||
|
muscle = 38.142612f,
|
||||||
|
bone = 3.9760387f
|
||||||
|
)
|
||||||
|
checkFixture(lib, w, imp, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun regression_female_40y_170cm_70kg_imp800() {
|
||||||
|
val lib = TrisaBodyAnalyzeLib(0, 40, 170f)
|
||||||
|
val w = 70f
|
||||||
|
val imp = 800f
|
||||||
|
val r = Fixture(
|
||||||
|
bmi = 24.221453f,
|
||||||
|
water = 47.909973f,
|
||||||
|
fat = 35.216103f,
|
||||||
|
muscle = 27.238312f,
|
||||||
|
bone = 3.7960525f
|
||||||
|
)
|
||||||
|
checkFixture(lib, w, imp, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helper --------------------------------------------------------------
|
||||||
|
|
||||||
|
private fun checkFixture(lib: TrisaBodyAnalyzeLib, w: Float, imp: Float, r: Fixture) {
|
||||||
|
assertThat(lib.getBMI(w)).isWithin(EPS).of(r.bmi)
|
||||||
|
assertThat(lib.getWater(w, imp)).isWithin(EPS).of(r.water)
|
||||||
|
assertThat(lib.getFat(w, imp)).isWithin(EPS).of(r.fat)
|
||||||
|
assertThat(lib.getMuscle(w, imp)).isWithin(EPS).of(r.muscle)
|
||||||
|
assertThat(lib.getBone(w, imp)).isWithin(EPS).of(r.bone)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class Fixture(
|
||||||
|
val bmi: Float,
|
||||||
|
val water: Float,
|
||||||
|
val fat: Float,
|
||||||
|
val muscle: Float,
|
||||||
|
val bone: Float
|
||||||
|
)
|
||||||
|
}
|
@@ -0,0 +1,194 @@
|
|||||||
|
/*
|
||||||
|
* openScale
|
||||||
|
* Copyright (C) 2025 olie.xdev <olie.xdeveloper@googlemail.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.health.openscale.core.bluetooth.libs
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.health.openscale.core.data.ActivityLevel
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class YunmaiLibTest {
|
||||||
|
|
||||||
|
private val EPS = 1e-3f
|
||||||
|
|
||||||
|
// --- Behavior (kept from earlier) ---------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun toYunmaiActivityLevel_mapsCorrectly() {
|
||||||
|
assertThat(YunmaiLib.toYunmaiActivityLevel(ActivityLevel.HEAVY)).isEqualTo(1)
|
||||||
|
assertThat(YunmaiLib.toYunmaiActivityLevel(ActivityLevel.EXTREME)).isEqualTo(1)
|
||||||
|
assertThat(YunmaiLib.toYunmaiActivityLevel(ActivityLevel.SEDENTARY)).isEqualTo(0)
|
||||||
|
assertThat(YunmaiLib.toYunmaiActivityLevel(ActivityLevel.MILD)).isEqualTo(0)
|
||||||
|
assertThat(YunmaiLib.toYunmaiActivityLevel(ActivityLevel.MODERATE)).isEqualTo(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun constructor_setsFitnessFlag_indirectlyVisibleInMuscleValues() {
|
||||||
|
val fit = YunmaiLib(1, 180f, ActivityLevel.EXTREME)
|
||||||
|
val normal = YunmaiLib(1, 180f, ActivityLevel.MILD)
|
||||||
|
val bf = 20f
|
||||||
|
assertThat(fit.getMuscle(bf)).isGreaterThan(normal.getMuscle(bf))
|
||||||
|
assertThat(fit.getSkeletalMuscle(bf)).isGreaterThan(normal.getSkeletalMuscle(bf))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Regression fixtures -------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun regression_male_mod_30y_180cm_80kg_res500_bf23() {
|
||||||
|
val lib = YunmaiLib(1, 180f, ActivityLevel.MODERATE)
|
||||||
|
val age = 30; val w = 80f; val res = 500; val bf = 23f
|
||||||
|
val r = Fx(55.907001f, 23.237043f, 51.595001f, 40.814999f, 3.263390f, 61.599998f, 11.318182f)
|
||||||
|
checkAll(lib, age, w, res, bf, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun regression_female_mild_28y_165cm_60kg_res520_bf28() {
|
||||||
|
val lib = YunmaiLib(0, 165f, ActivityLevel.MILD)
|
||||||
|
val age = 28; val w = 60f; val res = 520; val bf = 28f
|
||||||
|
val r = Fx(52.276997f, 29.947247f, 48.244999f, 38.164993f, 2.530795f, 43.200001f, 6.166667f)
|
||||||
|
checkAll(lib, age, w, res, bf, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun regression_male_sedentary_55y_175cm_95kg_res430_bf32() {
|
||||||
|
val lib = YunmaiLib(1, 175f, ActivityLevel.SEDENTARY)
|
||||||
|
val age = 55; val w = 95f; val res = 430; val bf = 32f
|
||||||
|
val r = Fx(49.372997f, 34.547203f, 45.564999f, 36.044998f, 3.365057f, 64.599998f, 18.590908f)
|
||||||
|
checkAll(lib, age, w, res, bf, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun regression_female_sedentary_55y_160cm_50kg_res600_bf27() {
|
||||||
|
val lib = YunmaiLib(0, 160f, ActivityLevel.SEDENTARY)
|
||||||
|
val age = 55; val w = 50f; val res = 600; val bf = 27f
|
||||||
|
val r = Fx(53.003002f, 28.532946f, 48.915001f, 38.694996f, 2.088284f, 36.500000f, 5.055555f)
|
||||||
|
checkAll(lib, age, w, res, bf, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun regression_male_heavy_20y_190cm_72kg_res480_bf14() {
|
||||||
|
val lib = YunmaiLib(1, 190f, ActivityLevel.HEAVY)
|
||||||
|
val age = 20; val w = 72f; val res = 480; val bf = 14f
|
||||||
|
val r = Fx(62.441002f, 15.266259f, 60.205002f, 51.605000f, 3.519648f, 61.919998f, 9.000000f)
|
||||||
|
checkAll(lib, age, w, res, bf, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun regression_female_mod_22y_155cm_55kg_res510_bf29() {
|
||||||
|
val lib = YunmaiLib(0, 155f, ActivityLevel.MODERATE)
|
||||||
|
val age = 22; val w = 55f; val res = 510; val bf = 29f
|
||||||
|
val r = Fx(51.551003f, 30.724077f, 47.575001f, 37.634998f, 2.187678f, 39.049999f, 6.722222f)
|
||||||
|
checkAll(lib, age, w, res, bf, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun regression_male_mild_35y_175cm_85kg_res200_bf25() {
|
||||||
|
val lib = YunmaiLib(1, 175f, ActivityLevel.MILD)
|
||||||
|
val age = 35; val w = 85f; val res = 200; val bf = 25f
|
||||||
|
val r = Fx(54.455002f, 27.232653f, 50.255001f, 39.754993f, 3.322063f, 63.750000f, 13.136364f)
|
||||||
|
checkAll(lib, age, w, res, bf, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun regression_female_sedentary_40y_170cm_70kg_res800_bf36() {
|
||||||
|
val lib = YunmaiLib(0, 170f, ActivityLevel.SEDENTARY)
|
||||||
|
val age = 40; val w = 70f; val res = 800; val bf = 36f
|
||||||
|
val r = Fx(46.468998f, 34.777931f, 42.884998f, 33.924999f, 2.674562f, 44.799999f, 10.409091f)
|
||||||
|
checkAll(lib, age, w, res, bf, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fixture dump helper (run manually) ----------------------------------
|
||||||
|
|
||||||
|
private data class DumpIn(
|
||||||
|
val sex: Int,
|
||||||
|
val heightCm: Float,
|
||||||
|
val activity: ActivityLevel,
|
||||||
|
val age: Int,
|
||||||
|
val weightKg: Float,
|
||||||
|
val resistance: Int,
|
||||||
|
val bodyFatPct: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun dump(fi: DumpIn) {
|
||||||
|
val lib = YunmaiLib(fi.sex, fi.heightCm, fi.activity)
|
||||||
|
val water = lib.getWater(fi.bodyFatPct)
|
||||||
|
val fat = lib.getFat(fi.age, fi.weightKg, fi.resistance)
|
||||||
|
val muscle = lib.getMuscle(fi.bodyFatPct)
|
||||||
|
val skeletal = lib.getSkeletalMuscle(fi.bodyFatPct)
|
||||||
|
val bone = lib.getBoneMass(muscle, fi.weightKg)
|
||||||
|
val lbm = lib.getLeanBodyMass(fi.weightKg, fi.bodyFatPct)
|
||||||
|
val vfat = lib.getVisceralFat(fi.bodyFatPct, fi.age)
|
||||||
|
|
||||||
|
println(
|
||||||
|
"""
|
||||||
|
// sex=${fi.sex}, height=${fi.heightCm}cm, act=${fi.activity}, age=${fi.age}, weight=${fi.weightKg}kg, res=${fi.resistance}, bf=${fi.bodyFatPct}%
|
||||||
|
Fixture(
|
||||||
|
water = ${"%.6f".format(water)}f,
|
||||||
|
fat = ${"%.6f".format(fat)}f,
|
||||||
|
muscle = ${"%.6f".format(muscle)}f,
|
||||||
|
skeletal = ${"%.6f".format(skeletal)}f,
|
||||||
|
bone = ${"%.6f".format(bone)}f,
|
||||||
|
lbm = ${"%.6f".format(lbm)}f,
|
||||||
|
visceralFat = ${"%.6f".format(vfat)}f
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dump_allFixtures() {
|
||||||
|
listOf(
|
||||||
|
DumpIn(1, 180f, ActivityLevel.MODERATE, 30, 80f, 500, 23f),
|
||||||
|
DumpIn(0, 165f, ActivityLevel.MILD, 28, 60f, 520, 28f),
|
||||||
|
DumpIn(1, 175f, ActivityLevel.SEDENTARY, 55, 95f, 430, 32f),
|
||||||
|
DumpIn(0, 160f, ActivityLevel.SEDENTARY, 55, 50f, 600, 27f),
|
||||||
|
DumpIn(1, 190f, ActivityLevel.HEAVY, 20, 72f, 480, 14f),
|
||||||
|
DumpIn(0, 155f, ActivityLevel.MODERATE, 22, 55f, 510, 29f),
|
||||||
|
DumpIn(1, 175f, ActivityLevel.MILD, 35, 85f, 200, 25f),
|
||||||
|
DumpIn(0, 170f, ActivityLevel.SEDENTARY, 40, 70f, 800, 36f)
|
||||||
|
).forEach(::dump)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helpers -------------------------------------------------------------
|
||||||
|
|
||||||
|
private data class Fx(
|
||||||
|
val water: Float,
|
||||||
|
val fat: Float,
|
||||||
|
val muscle: Float,
|
||||||
|
val skeletal: Float,
|
||||||
|
val bone: Float,
|
||||||
|
val lbm: Float,
|
||||||
|
val visceralFat: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun checkAll(
|
||||||
|
lib: YunmaiLib,
|
||||||
|
age: Int,
|
||||||
|
w: Float,
|
||||||
|
res: Int,
|
||||||
|
bf: Float,
|
||||||
|
r: Fx
|
||||||
|
) {
|
||||||
|
assertThat(lib.getWater(bf)).isWithin(EPS).of(r.water)
|
||||||
|
assertThat(lib.getFat(age, w, res)).isWithin(EPS).of(r.fat)
|
||||||
|
assertThat(lib.getMuscle(bf)).isWithin(EPS).of(r.muscle)
|
||||||
|
assertThat(lib.getSkeletalMuscle(bf)).isWithin(EPS).of(r.skeletal)
|
||||||
|
assertThat(lib.getBoneMass(r.muscle, w)).isWithin(EPS).of(r.bone)
|
||||||
|
assertThat(lib.getLeanBodyMass(w, bf)).isWithin(EPS).of(r.lbm)
|
||||||
|
assertThat(lib.getVisceralFat(bf, age)).isWithin(EPS).of(r.visceralFat)
|
||||||
|
}
|
||||||
|
}
|
@@ -23,8 +23,10 @@ compose-material = "1.7.8"
|
|||||||
constraintlayout-compose = "1.1.1"
|
constraintlayout-compose = "1.1.1"
|
||||||
glance = "1.1.1"
|
glance = "1.1.1"
|
||||||
kotlinCsv = "1.10.0"
|
kotlinCsv = "1.10.0"
|
||||||
blessedKotlin = "3.0.9"
|
blessedKotlin = "3.0.10"
|
||||||
blessedJava = "2.5.2"
|
blessedJava = "2.5.2"
|
||||||
|
truth = "1.4.2"
|
||||||
|
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@@ -66,6 +68,7 @@ androidx-glance-material3 = { group = "androidx.glance", name = "glance-material
|
|||||||
kotlin-csv-jvm = { group = "com.github.doyaaaaaken", name = "kotlin-csv-jvm", version.ref = "kotlinCsv" }
|
kotlin-csv-jvm = { group = "com.github.doyaaaaaken", name = "kotlin-csv-jvm", version.ref = "kotlinCsv" }
|
||||||
blessed-kotlin = { group = "com.github.weliem", name = "blessed-kotlin", version.ref = "blessedKotlin" }
|
blessed-kotlin = { group = "com.github.weliem", name = "blessed-kotlin", version.ref = "blessedKotlin" }
|
||||||
blessed-java = { group = "com.github.weliem", name = "blessed-android", version.ref = "blessedJava" }
|
blessed-java = { group = "com.github.weliem", name = "blessed-android", version.ref = "blessedJava" }
|
||||||
|
truth = { group = "com.google.truth", name = "truth", version.ref = "truth" }
|
||||||
|
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
Reference in New Issue
Block a user