Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ac415251 authored by Ellen Poe's avatar Ellen Poe
Browse files

refactor: reduce cognitive complexity in opening hours code

parent 11403139
Loading
Loading
Loading
Loading
Loading
+172 −140
Original line number Diff line number Diff line
@@ -64,6 +64,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import ch.poole.openinghoursparser.OpeningHoursParseException
import ch.poole.openinghoursparser.OpeningHoursParser
import ch.poole.openinghoursparser.Rule
import ch.poole.openinghoursparser.WeekDayRange
import earth.maps.cardinal.R.dimen
import earth.maps.cardinal.R.drawable
@@ -189,32 +190,22 @@ fun getOpeningHoursForNext7Days(
    return dayOpeningHours
}

fun ordinalInRange(ord: Int, start: Int, end: Int): Boolean {
    return ord >= start && ord <= end
}

fun weekdayRangeIncludesDay(range: WeekDayRange, day: Int): Boolean {
    if (range.startDay != null && range.startDay.ordinal == day) {
        return true
    } else if (range.startDay == null || range.endDay == null) {
        return false
    }
    if (range.endDay < range.startDay) {
        for (ord in 0..range.endDay.ordinal) {
            if (ord == day) {
                return true
            }
        }
        for (ord in range.startDay.ordinal..6) {
            if (ord == day) {
                return true
            }
        }
    return if (range.endDay < range.startDay) {
        ordinalInRange(day, 0, range.endDay.ordinal) || ordinalInRange(day, range.startDay.ordinal, 6)
    } else {
        for (ord in range.startDay.ordinal..range.endDay.ordinal) {
            if (ord == day) {
                return true
        ordinalInRange(day, range.startDay.ordinal, range.endDay.ordinal)
    }
}
    }
    return false
}

@OptIn(ExperimentalTime::class)
@Composable
@@ -245,54 +236,8 @@ fun ExpandableOpeningHours(
                .padding(16.dp)
        ) {
            // Header with current status and expand/collapse icon
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .clickable { expanded = !expanded },
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Column(
                    modifier = Modifier.weight(1f)
                ) {
                    Text(
                        text = stringResource(string.opening_hours_title),
                        style = MaterialTheme.typography.titleMedium,
                        fontWeight = FontWeight.Medium
                    )

                    currentStatus?.let { status ->
                        Row(
                            modifier = Modifier.padding(top = 4.dp),
                            verticalAlignment = Alignment.CenterVertically
                        ) {
                            Text(
                                text = status.statusText,
                                color = status.statusColor,
                                style = MaterialTheme.typography.bodyMedium
                            )
                            status.nextTimeText?.let { nextTime ->
                                Spacer(modifier = Modifier.width(4.dp))
                                Text(
                                    text = nextTime,
                                    style = MaterialTheme.typography.bodyMedium,
                                    color = MaterialTheme.colorScheme.onSurfaceVariant
                                )
                            }
                        }
                    }
                }

                Icon(
                    painter = painterResource(
                        if (expanded) drawable.ic_arrow_down else drawable.ic_arrow_down
                    ),
                    contentDescription = stringResource(
                        if (expanded) string.content_description_collapse_opening_hours
                        else string.content_description_expand_opening_hours
                    ),
                    modifier = Modifier.size(24.dp)
                )
            OpeningHoursHeader(expanded, currentStatus) {
                expanded = it
            }

            // Expanded content with table
@@ -300,24 +245,7 @@ fun ExpandableOpeningHours(
                Spacer(modifier = Modifier.height(8.dp))

                // Table header
                Row(
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Text(
                        text = "Day",
                        modifier = Modifier.weight(1f),
                        style = MaterialTheme.typography.bodySmall,
                        fontWeight = FontWeight.Medium,
                        color = MaterialTheme.colorScheme.onSurfaceVariant
                    )
                    Text(
                        text = "Hours",
                        modifier = Modifier.weight(2f),
                        style = MaterialTheme.typography.bodySmall,
                        fontWeight = FontWeight.Medium,
                        color = MaterialTheme.colorScheme.onSurfaceVariant
                    )
                }
                OpeningHoursTableHeader()

                HorizontalDivider(
                    modifier = Modifier.padding(vertical = 4.dp),
@@ -326,6 +254,21 @@ fun ExpandableOpeningHours(

                // Table rows for each day
                openingHoursData.forEach { dayHours ->
                    OpeningHoursTableRow(dayHours)

                    if (dayHours != openingHoursData.last()) {
                        HorizontalDivider(
                            color = MaterialTheme.colorScheme.outlineVariant
                        )
                    }
                }
            }
        }
    }
}

@Composable
private fun OpeningHoursTableRow(dayHours: DayOpeningHours) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
@@ -351,19 +294,88 @@ fun ExpandableOpeningHours(
            color = MaterialTheme.colorScheme.onSurface
        )
    }
}

                    if (dayHours != openingHoursData.last()) {
                        HorizontalDivider(
                            color = MaterialTheme.colorScheme.outlineVariant
@Composable
private fun OpeningHoursTableHeader() {
    Row(
        modifier = Modifier.fillMaxWidth()
    ) {
        Text(
            text = stringResource(string.opening_hours_day),
            modifier = Modifier.weight(1f),
            style = MaterialTheme.typography.bodySmall,
            fontWeight = FontWeight.Medium,
            color = MaterialTheme.colorScheme.onSurfaceVariant
        )
        Text(
            text = stringResource(string.opening_hours_hours),
            modifier = Modifier.weight(2f),
            style = MaterialTheme.typography.bodySmall,
            fontWeight = FontWeight.Medium,
            color = MaterialTheme.colorScheme.onSurfaceVariant
        )
    }
}

@Composable
private fun OpeningHoursHeader(
    expanded: Boolean,
    currentStatus: OpeningStatusDisplay?,
    onExpandChanged: (Boolean) -> Unit,
) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clickable { onExpandChanged(!expanded) },
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Column(
            modifier = Modifier.weight(1f)
        ) {
            Text(
                text = stringResource(string.opening_hours_title),
                style = MaterialTheme.typography.titleMedium,
                fontWeight = FontWeight.Medium
            )

            currentStatus?.let { status ->
                Row(
                    modifier = Modifier.padding(top = 4.dp),
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    Text(
                        text = status.statusText,
                        color = status.statusColor,
                        style = MaterialTheme.typography.bodyMedium
                    )
                    status.nextTimeText?.let { nextTime ->
                        Spacer(modifier = Modifier.width(4.dp))
                        Text(
                            text = nextTime,
                            style = MaterialTheme.typography.bodyMedium,
                            color = MaterialTheme.colorScheme.onSurfaceVariant
                        )
                    }
                }
            }
        }

data class OpeningStatus(
        Icon(
            painter = painterResource(
                drawable.ic_arrow_down
            ),
            contentDescription = stringResource(
                if (expanded) string.content_description_collapse_opening_hours
                else string.content_description_expand_opening_hours
            ),
            modifier = Modifier.size(24.dp)
        )
    }
}

data class OpeningStatusDisplay(
    val statusText: String,
    val statusColor: Color,
    val nextTimeText: String?
@@ -376,7 +388,7 @@ fun getCurrentOpeningStatus(
    now: LocalDateTime,
    timeZone: TimeZone,
    use24HourFormat: Boolean
): OpeningStatus? {
): OpeningStatusDisplay? {
    place.openingHours?.let { openingHours ->
        val today = now.dayOfWeek
        val parser =
@@ -391,41 +403,17 @@ fun getCurrentOpeningStatus(
        val timeOfDay = now.time
        val minutesFromMidnight = timeOfDay.minute + timeOfDay.hour * 60

        var isOpen = false
        var openingTimeToday: Int? = null
        var closingTimeToday: Int? = null
        val openingStatus = processOpeningHoursRules(rules, today, minutesFromMidnight)

        for (rule in rules) {
            val days = rule.days
            val times = rule.times
            if (days == null || times == null) {
                continue
            }
            for (dayRule in days) {
                if (weekdayRangeIncludesDay(dayRule, today.ordinal)) {
                    for (timeRule in times) {
                        if (timeRule.start < minutesFromMidnight && timeRule.end > minutesFromMidnight) {
                            isOpen = true
                            if (closingTimeToday == null || timeRule.end < closingTimeToday) {
                                closingTimeToday = timeRule.end
                            }
                        } else if (openingTimeToday == null || timeRule.start < openingTimeToday) {
                            openingTimeToday = timeRule.start
                        }
                    }
                }
            }
        }

        val openingInstantToday = openingTimeToday?.let { openingTime ->
        val openingInstantToday = openingStatus.openingTimeToday?.let { openingTime ->
            now.date.atTime(0, 0).toInstant(timeZone = timeZone).plus(openingTime.minutes)
        }
        val closingInstantToday = closingTimeToday?.let { closingTime ->
        val closingInstantToday = openingStatus.closingTimeToday?.let { closingTime ->
            now.date.atTime(0, 0).toInstant(timeZone = timeZone).plus(closingTime.minutes)
        }

        return if (isOpen) {
            OpeningStatus(
        return if (openingStatus.isOpen) {
            OpeningStatusDisplay(
                statusText = stringResource(string.opening_hours_open),
                statusColor = Color.Green,
                nextTimeText = closingInstantToday?.let { closingInstant ->
@@ -436,7 +424,7 @@ fun getCurrentOpeningStatus(
                }
            )
        } else {
            OpeningStatus(
            OpeningStatusDisplay(
                statusText = stringResource(string.opening_hours_closed),
                statusColor = Color.Red,
                nextTimeText = openingInstantToday?.let { openingInstant ->
@@ -451,6 +439,50 @@ fun getCurrentOpeningStatus(
    return null
}

data class OpeningStatus(
    val isOpen: Boolean,
    val closingTimeToday: Int?,
    val openingTimeToday: Int?,

)

@Suppress("CognitiveComplexMethod")
private fun processOpeningHoursRules(
    rules: List<Rule>,
    today: DayOfWeek,
    minutesFromMidnight: Int,
): OpeningStatus {
    var isOpen = false
    var closingTimeToday: Int? = null
    var openingTimeToday: Int? = null
    for (rule in rules) {
        val days = rule.days
        val times = rule.times
        if (days == null || times == null) {
            continue
        }
        for (dayRule in days) {
            if (weekdayRangeIncludesDay(dayRule, today.ordinal)) {
                for (timeRule in times) {
                    if (timeRule.start < minutesFromMidnight && timeRule.end > minutesFromMidnight) {
                        isOpen = true
                        if (closingTimeToday == null || timeRule.end < closingTimeToday) {
                            closingTimeToday = timeRule.end
                        }
                    } else if (openingTimeToday == null || timeRule.start < openingTimeToday) {
                        openingTimeToday = timeRule.start
                    }
                }
            }
        }
    }
    return OpeningStatus(
        isOpen,
        closingTimeToday,
        openingTimeToday,
    )
}

@OptIn(ExperimentalTime::class)
@Composable
fun PlaceCardScreen(
+2 −0
Original line number Diff line number Diff line
@@ -284,4 +284,6 @@
    <string name="day_sunday">Sunday</string>

    <string name="day_today">Today</string>
    <string name="opening_hours_hours">Hours</string>
    <string name="opening_hours_day">Day</string>
</resources>