mirror of
https://github.com/oliexdev/openScale.git
synced 2025-08-26 09:44:31 +02:00
Handle 'dateTime' column and skip zero values during CSV import for compatibility with openScale 2.x CSV exports (see issue #1156).
This commit is contained in:
@@ -58,6 +58,7 @@ import java.time.format.DateTimeFormatter
|
|||||||
import java.time.format.DateTimeFormatterBuilder
|
import java.time.format.DateTimeFormatterBuilder
|
||||||
import java.time.format.DateTimeParseException
|
import java.time.format.DateTimeParseException
|
||||||
import java.time.temporal.ChronoField
|
import java.time.temporal.ChronoField
|
||||||
|
import java.time.temporal.TemporalQueries.localDate
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
@@ -325,6 +326,8 @@ class SettingsViewModel(
|
|||||||
|
|
||||||
val dateColumnKey = MeasurementTypeKey.DATE.name
|
val dateColumnKey = MeasurementTypeKey.DATE.name
|
||||||
val timeColumnKey = MeasurementTypeKey.TIME.name
|
val timeColumnKey = MeasurementTypeKey.TIME.name
|
||||||
|
val dateTimeColumnKey = "dateTime"
|
||||||
|
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
|
||||||
|
|
||||||
val newMeasurementsToSave = mutableListOf<Pair<Measurement, List<MeasurementValue>>>()
|
val newMeasurementsToSave = mutableListOf<Pair<Measurement, List<MeasurementValue>>>()
|
||||||
|
|
||||||
@@ -337,6 +340,7 @@ class SettingsViewModel(
|
|||||||
var header: List<String>? = null
|
var header: List<String>? = null
|
||||||
var dateColumnIndex = -1
|
var dateColumnIndex = -1
|
||||||
var timeColumnIndex = -1
|
var timeColumnIndex = -1
|
||||||
|
var dateTimeColumnIndex = -1
|
||||||
val valueColumnMap = mutableMapOf<Int, MeasurementType>()
|
val valueColumnMap = mutableMapOf<Int, MeasurementType>()
|
||||||
|
|
||||||
readAllAsSequence().forEachIndexed { rowIndex, row ->
|
readAllAsSequence().forEachIndexed { rowIndex, row ->
|
||||||
@@ -344,9 +348,10 @@ class SettingsViewModel(
|
|||||||
header = row
|
header = row
|
||||||
dateColumnIndex = header.indexOfFirst { it.equals(dateColumnKey, ignoreCase = true) }
|
dateColumnIndex = header.indexOfFirst { it.equals(dateColumnKey, ignoreCase = true) }
|
||||||
timeColumnIndex = header.indexOfFirst { it.equals(timeColumnKey, ignoreCase = true) }
|
timeColumnIndex = header.indexOfFirst { it.equals(timeColumnKey, ignoreCase = true) }
|
||||||
|
dateTimeColumnIndex = header.indexOfFirst { it.equals(dateTimeColumnKey, ignoreCase = true) }
|
||||||
|
|
||||||
if (dateColumnIndex == -1) {
|
if (dateColumnIndex == -1 && dateTimeColumnIndex == -1) {
|
||||||
LogManager.e(TAG, "CSV import for user $userId: Mandatory date column (expected: '$dateColumnKey') not found in header: $header. Aborting import.")
|
LogManager.e(TAG, "CSV import for user $userId: Mandatory date information (expected '$dateColumnKey' or '$dateTimeColumnKey') not found in header: $header. Aborting import.")
|
||||||
sharedViewModel.showSnackbar(R.string.import_error_missing_date_column, listOf(dateColumnKey))
|
sharedViewModel.showSnackbar(R.string.import_error_missing_date_column, listOf(dateColumnKey))
|
||||||
_isLoadingImport.value = false
|
_isLoadingImport.value = false
|
||||||
return@forEachIndexed // Exit the coroutine
|
return@forEachIndexed // Exit the coroutine
|
||||||
@@ -355,7 +360,7 @@ class SettingsViewModel(
|
|||||||
|
|
||||||
header.forEachIndexed { colIdx, csvColumnName ->
|
header.forEachIndexed { colIdx, csvColumnName ->
|
||||||
// Skip date and time columns as they are handled separately
|
// Skip date and time columns as they are handled separately
|
||||||
if (colIdx == dateColumnIndex || colIdx == timeColumnIndex) {
|
if (colIdx == dateColumnIndex || colIdx == timeColumnIndex || colIdx == dateTimeColumnIndex) {
|
||||||
return@forEachIndexed
|
return@forEachIndexed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,36 +401,51 @@ class SettingsViewModel(
|
|||||||
|
|
||||||
if (header == null) throw IOException("CSV header not found or processed.") // Should not happen
|
if (header == null) throw IOException("CSV header not found or processed.") // Should not happen
|
||||||
|
|
||||||
val dateString = row.getOrNull(dateColumnIndex)
|
var parsedLocalDate: LocalDate? = null
|
||||||
if (dateString.isNullOrBlank()) {
|
var parsedLocalTime: LocalTime? = null
|
||||||
LogManager.w(TAG, "CSV import for user $userId: Row ${rowIndex + 1} skipped: Date value is missing in mandatory column '$dateColumnKey'.")
|
val dateTimeString = if (dateTimeColumnIndex != -1) row.getOrNull(dateTimeColumnIndex) else null
|
||||||
linesSkippedMissingDate++
|
val dateString = if (dateColumnIndex != -1) row.getOrNull(dateColumnIndex) else null
|
||||||
return@forEachIndexed
|
val timeStringFromColumn = if (timeColumnIndex != -1) row.getOrNull(timeColumnIndex) else null
|
||||||
}
|
|
||||||
// ... (rest of row processing, date/time parsing, value extraction)
|
|
||||||
val localDate = try {
|
|
||||||
LocalDate.parse(dateString, dateFormatter)
|
|
||||||
} catch (e: DateTimeParseException) {
|
|
||||||
LogManager.w(TAG, "CSV import for user $userId: Error parsing date '$dateString' (expected YYYY-MM-DD) in row ${rowIndex + 1}. Skipping row.", e)
|
|
||||||
linesSkippedDateParseError++
|
|
||||||
return@forEachIndexed
|
|
||||||
}
|
|
||||||
|
|
||||||
val timeString = if (timeColumnIndex != -1) row.getOrNull(timeColumnIndex) else null
|
if (!dateTimeString.isNullOrBlank()) {
|
||||||
val localTime: LocalTime = if (timeString.isNullOrBlank()) {
|
try {
|
||||||
LocalTime.NOON // Default if time is missing or blank
|
val tempDateTime = LocalDateTime.parse(dateTimeString, dateTimeFormatter)
|
||||||
} else {
|
parsedLocalDate = tempDateTime.toLocalDate()
|
||||||
try { LocalTime.parse(timeString, timeFormatter) }
|
parsedLocalTime = tempDateTime.toLocalTime()
|
||||||
catch (e1: DateTimeParseException) {
|
LogManager.d(TAG, "CSV import for user $userId: Row ${rowIndex + 1} parsed dateTime: $dateTimeString -> Date: $parsedLocalDate, Time: $parsedLocalTime")
|
||||||
try { LocalTime.parse(timeString, flexibleTimeFormatter) }
|
} catch (e: DateTimeParseException) {
|
||||||
catch (e2: DateTimeParseException) {
|
LogManager.w(TAG, "CSV import for user $userId: Error parsing dateTime '$dateTimeString' (expected YYYY-MM-DD HH:mm) in row ${rowIndex + 1}. Trying separate date/time columns or skipping.", e)
|
||||||
LogManager.w(TAG, "CSV import for user $userId: Time '$timeString' in row ${rowIndex + 1} could not be parsed. Using default.", e2)
|
|
||||||
LocalTime.NOON
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val localDateTime = LocalDateTime.of(localDate, localTime)
|
if (parsedLocalDate == null && !dateString.isNullOrBlank()) {
|
||||||
|
try {
|
||||||
|
parsedLocalDate = LocalDate.parse(dateString, dateFormatter)
|
||||||
|
LogManager.d(TAG, "CSV import for user $userId: Row ${rowIndex + 1} parsed dateString: $dateString -> Date: $parsedLocalDate")
|
||||||
|
} catch (e: DateTimeParseException) {
|
||||||
|
LogManager.w(TAG, "CSV import for user $userId: Error parsing date '$dateString' (expected YYYY-MM-DD) in row ${rowIndex + 1}. Skipping row.", e)
|
||||||
|
linesSkippedDateParseError++
|
||||||
|
return@forEachIndexed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedLocalTime == null) {
|
||||||
|
parsedLocalTime = if (timeStringFromColumn.isNullOrBlank()) {
|
||||||
|
LocalTime.NOON
|
||||||
|
} else {
|
||||||
|
try { LocalTime.parse(timeStringFromColumn, timeFormatter) }
|
||||||
|
catch (e1: DateTimeParseException) {
|
||||||
|
try { LocalTime.parse(timeStringFromColumn, flexibleTimeFormatter) }
|
||||||
|
catch (e2: DateTimeParseException) {
|
||||||
|
LogManager.w(TAG, "CSV import for user $userId: Time '$timeStringFromColumn' in row ${rowIndex + 1} could not be parsed. Using default.", e2)
|
||||||
|
LocalTime.NOON
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogManager.d(TAG, "CSV import for user $userId: Row ${rowIndex + 1} parsed timeString: $timeStringFromColumn -> Time: $parsedLocalTime")
|
||||||
|
}
|
||||||
|
|
||||||
|
val localDateTime = LocalDateTime.of(parsedLocalDate, parsedLocalTime)
|
||||||
val timestampMillis = localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
|
val timestampMillis = localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
|
||||||
val measurement = Measurement(userId = userId, timestamp = timestampMillis)
|
val measurement = Measurement(userId = userId, timestamp = timestampMillis)
|
||||||
val measurementValues = mutableListOf<MeasurementValue>()
|
val measurementValues = mutableListOf<MeasurementValue>()
|
||||||
@@ -434,28 +454,73 @@ class SettingsViewModel(
|
|||||||
val valueString = row.getOrNull(colIdx)
|
val valueString = row.getOrNull(colIdx)
|
||||||
if (!valueString.isNullOrBlank()) {
|
if (!valueString.isNullOrBlank()) {
|
||||||
try {
|
try {
|
||||||
val mv = MeasurementValue(
|
var skipThisValue = false
|
||||||
typeId = type.id,
|
|
||||||
measurementId = 0, // Will be set by Room
|
val floatVal = if (type.inputType == InputFieldType.FLOAT) valueString.toFloatOrNull() else null
|
||||||
textValue = if (type.inputType == InputFieldType.TEXT) valueString else null,
|
val intVal = if (type.inputType == InputFieldType.INT) valueString.toIntOrNull() else null
|
||||||
floatValue = if (type.inputType == InputFieldType.FLOAT) valueString.toFloatOrNull() else null,
|
|
||||||
intValue = if (type.inputType == InputFieldType.INT) valueString.toIntOrNull() else null,
|
if (type.inputType == InputFieldType.FLOAT && floatVal == 0.0f) {
|
||||||
dateValue = when (type.inputType) {
|
LogManager.d(TAG, "CSV import for user $userId: Skipping value '0' for FLOAT type '${type.key.name}' in row ${rowIndex + 1}.")
|
||||||
InputFieldType.DATE -> LocalDate.parse(valueString, dateFormatter).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli()
|
skipThisValue = true
|
||||||
InputFieldType.TIME -> {
|
}
|
||||||
val parsedTime = try { LocalTime.parse(valueString, timeFormatter) } catch (e: Exception) { LocalTime.parse(valueString, flexibleTimeFormatter) }
|
if (type.inputType == InputFieldType.INT && intVal == 0) {
|
||||||
parsedTime.atDate(LocalDate.of(1970, 1, 1)).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() }
|
LogManager.d(TAG, "CSV import for user $userId: Skipping value '0' for INT type '${type.key.name}' in row ${rowIndex + 1}.")
|
||||||
else -> null
|
skipThisValue = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!skipThisValue) {
|
||||||
|
val mv = MeasurementValue(
|
||||||
|
typeId = type.id,
|
||||||
|
measurementId = 0, // Will be set by Room
|
||||||
|
textValue = if (type.inputType == InputFieldType.TEXT) valueString else null,
|
||||||
|
floatValue = if (type.inputType == InputFieldType.FLOAT) valueString.toFloatOrNull() else null,
|
||||||
|
intValue = if (type.inputType == InputFieldType.INT) valueString.toIntOrNull() else null,
|
||||||
|
dateValue = when (type.inputType) {
|
||||||
|
InputFieldType.DATE -> LocalDate.parse(
|
||||||
|
valueString,
|
||||||
|
dateFormatter
|
||||||
|
).atStartOfDay(ZoneId.systemDefault())
|
||||||
|
.toInstant().toEpochMilli()
|
||||||
|
|
||||||
|
InputFieldType.TIME -> {
|
||||||
|
val parsedTime = try {
|
||||||
|
LocalTime.parse(
|
||||||
|
valueString,
|
||||||
|
timeFormatter
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LocalTime.parse(
|
||||||
|
valueString,
|
||||||
|
flexibleTimeFormatter
|
||||||
|
)
|
||||||
|
}
|
||||||
|
parsedTime.atDate(
|
||||||
|
LocalDate.of(
|
||||||
|
1970,
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
).atZone(ZoneId.systemDefault())
|
||||||
|
.toInstant().toEpochMilli()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
var isValidValue = true
|
||||||
|
if (type.inputType == InputFieldType.FLOAT && mv.floatValue == null) isValidValue =
|
||||||
|
false
|
||||||
|
if (type.inputType == InputFieldType.INT && mv.intValue == null) isValidValue =
|
||||||
|
false
|
||||||
|
if (isValidValue) {
|
||||||
|
measurementValues.add(mv)
|
||||||
|
} else {
|
||||||
|
LogManager.w(
|
||||||
|
TAG,
|
||||||
|
"CSV import for user $userId: Could not parse value '$valueString' for type '${type.key.name}' in row ${rowIndex + 1}."
|
||||||
|
)
|
||||||
|
valuesSkippedParseError++
|
||||||
}
|
}
|
||||||
)
|
|
||||||
var isValidValue = true
|
|
||||||
if (type.inputType == InputFieldType.FLOAT && mv.floatValue == null) isValidValue = false
|
|
||||||
if (type.inputType == InputFieldType.INT && mv.intValue == null) isValidValue = false
|
|
||||||
if (isValidValue) {
|
|
||||||
measurementValues.add(mv)
|
|
||||||
} else {
|
|
||||||
LogManager.w(TAG, "CSV import for user $userId: Could not parse value '$valueString' for type '${type.key.name}' in row ${rowIndex + 1}.")
|
|
||||||
valuesSkippedParseError++
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
LogManager.w(TAG, "CSV import for user $userId: Error processing value '$valueString' for type '${type.key.name}' in row ${rowIndex + 1}.", e)
|
LogManager.w(TAG, "CSV import for user $userId: Error processing value '$valueString' for type '${type.key.name}' in row ${rowIndex + 1}.", e)
|
||||||
|
Reference in New Issue
Block a user