-
Notifications
You must be signed in to change notification settings - Fork 133
[WOOMOB-1515] - Format booking duration following web's logic #14760
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,7 @@ import com.woocommerce.android.ui.bookings.compose.BookingSummaryModel | |
import com.woocommerce.android.ui.bookings.details.CancelStatus | ||
import com.woocommerce.android.ui.bookings.list.BookingListItem | ||
import com.woocommerce.android.util.CurrencyFormatter | ||
import com.woocommerce.android.viewmodel.ResourceProvider | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.withContext | ||
import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingCustomerInfo | ||
|
@@ -29,7 +30,8 @@ import javax.inject.Inject | |
|
||
class BookingMapper @Inject constructor( | ||
private val currencyFormatter: CurrencyFormatter, | ||
private val getLocations: GetLocations | ||
private val getLocations: GetLocations, | ||
private val resourceProvider: ResourceProvider | ||
) { | ||
private val summaryDateFormatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime( | ||
FormatStyle.MEDIUM, | ||
|
@@ -62,16 +64,18 @@ class BookingMapper @Inject constructor( | |
staffMemberStatus: BookingStaffMemberStatus?, | ||
cancelStatus: CancelStatus, | ||
): BookingAppointmentDetailsModel { | ||
val durationMinutes = Duration.between(start, end).toMinutes() | ||
val duration = Duration.between(start, end) | ||
.normalizeBookingDuration() | ||
.toHumanReadableFormat() | ||
return BookingAppointmentDetailsModel( | ||
date = detailsDateFormatter.format(start), | ||
time = "${timeRangeFormatter.format(start)} - ${timeRangeFormatter.format(end)}", | ||
staff = staffMemberStatus, | ||
// TODO replace mocked values when available from API | ||
location = "238 Willow Creek Drive, Montgomery AL 36109", | ||
duration = "$durationMinutes min", | ||
price = currencyFormatter.formatCurrency(cost, currency), | ||
cancelStatus = cancelStatus, | ||
duration = duration, | ||
) | ||
} | ||
|
||
|
@@ -131,6 +135,110 @@ class BookingMapper @Inject constructor( | |
) | ||
} | ||
|
||
/** | ||
* Normalize booking duration by adjusting for precision issues. | ||
* | ||
* This function handles cases where a booking duration is very close to | ||
* common time boundaries (days/hours) but falls short due to precision issues. | ||
* It rounds up durations that are within one minute of these boundaries. | ||
*/ | ||
private fun Duration.normalizeBookingDuration(): Duration { | ||
val dayInSeconds = Duration.ofDays(1).seconds | ||
val hourInSeconds = Duration.ofHours(1).seconds | ||
val minuteInSeconds = Duration.ofMinutes(1).seconds | ||
|
||
var durationInSeconds = this.seconds | ||
val boundaries = listOf(dayInSeconds, hourInSeconds) | ||
for (boundary in boundaries) { | ||
val remainder = durationInSeconds % boundary | ||
val difference = if (remainder == 0L) 0L else boundary - remainder | ||
if (difference > 0 && difference <= minuteInSeconds) { | ||
durationInSeconds += difference | ||
} | ||
} | ||
return Duration.ofSeconds(durationInSeconds) | ||
} | ||
|
||
@Suppress("LongMethod") | ||
private fun Duration.toHumanReadableFormat(): String { | ||
Comment on lines
+162
to
+163
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nitpick] The Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think AI has a point here, the method is quite long, and the multiple branches complicate the code a bit. I'm suggesting a simpler version below, please check it out: private fun Duration.toHumanReadableFormat(): String {
if (this < Duration.ofMinutes(1)) {
return resourceProvider.getQuantityString(
quantity = seconds.toInt(),
default = R.string.booking_duration_seconds,
one = R.string.booking_duration_second
)
}
val days = toDays()
val hours = minusDays(days).toHours()
val minutes = minusDays(days).minusHours(hours).toMinutes()
return buildString {
if (days > 0) {
append(
resourceProvider.getQuantityString(
quantity = days.toInt(),
default = R.string.booking_duration_days,
one = R.string.booking_duration_day
)
)
}
if (hours > 0) {
append(" ")
append(
resourceProvider.getQuantityString(
quantity = hours.toInt(),
default = R.string.booking_duration_hours,
one = R.string.booking_duration_hour
)
)
}
if (minutes > 0) {
append(" ")
append(
resourceProvider.getQuantityString(
quantity = minutes.toInt(),
default = R.string.booking_duration_minutes,
one = R.string.booking_duration_minute
)
)
}
}.trim()
} This gives the same results, and the tests are green. But please feel free to ignore the suggestion if you prefer the other version 🙂 |
||
val totalSeconds = seconds | ||
val dayInSeconds = Duration.ofDays(1).toSeconds() | ||
val hourInSeconds = Duration.ofHours(1).toSeconds() | ||
val minuteInSeconds = Duration.ofMinutes(1).toSeconds() | ||
|
||
return when { | ||
totalSeconds >= dayInSeconds -> { | ||
val days = (totalSeconds / dayInSeconds).toInt() | ||
val hours = ((totalSeconds % dayInSeconds) / hourInSeconds).toInt() | ||
val minutes = ((totalSeconds % hourInSeconds) / minuteInSeconds).toInt() | ||
|
||
val parts = mutableListOf<String>() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. np, we can probably use buildString {
append(
resourceProvider.getQuantityString(
quantity = days,
default = R.string.booking_duration_days,
one = R.string.booking_duration_day
)
)
if (hours > 0) {
append(" ")
append(
resourceProvider.getQuantityString(
quantity = hours,
default = R.string.booking_duration_hours,
one = R.string.booking_duration_hour
)
)
}
if (minutes > 0) {
append(" ")
append(
resourceProvider.getQuantityString(
quantity = minutes,
default = R.string.booking_duration_minutes,
one = R.string.booking_duration_minute
)
)
}
} |
||
parts += resourceProvider.getQuantityString( | ||
quantity = days, | ||
default = R.string.booking_duration_days, | ||
one = R.string.booking_duration_day | ||
) | ||
if (hours > 0) { | ||
parts += resourceProvider.getQuantityString( | ||
quantity = hours, | ||
default = R.string.booking_duration_hours, | ||
one = R.string.booking_duration_hour | ||
) | ||
} | ||
if (minutes > 0) { | ||
parts += resourceProvider.getQuantityString( | ||
quantity = minutes, | ||
default = R.string.booking_duration_minutes, | ||
one = R.string.booking_duration_minute | ||
) | ||
} | ||
parts.joinToString(separator = " ") | ||
} | ||
|
||
totalSeconds >= hourInSeconds -> { | ||
val hours = (totalSeconds / hourInSeconds).toInt() | ||
val minutes = ((totalSeconds % hourInSeconds) / minuteInSeconds).toInt() | ||
if (minutes == 0) { | ||
resourceProvider.getQuantityString( | ||
quantity = hours, | ||
default = R.string.booking_duration_hours, | ||
one = R.string.booking_duration_hour | ||
) | ||
} else { | ||
val hoursPart = resourceProvider.getQuantityString( | ||
quantity = hours, | ||
default = R.string.booking_duration_hours, | ||
one = R.string.booking_duration_hour | ||
) | ||
Comment on lines
+208
to
+212
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure this is enough for other languages that we support, but from what I've checked, this is how we do plurals here. For example, that wouldn't work for Polish, but we don't support Polish, so maybe that's fine. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, our plurals support is quite limited, a limitation of Glotpress, this won't work well for Arabic too, but I don't think we have much to do here. |
||
val minutesPart = resourceProvider.getQuantityString( | ||
quantity = minutes, | ||
default = R.string.booking_duration_minutes, | ||
one = R.string.booking_duration_minute | ||
) | ||
"$hoursPart $minutesPart" | ||
} | ||
} | ||
|
||
totalSeconds >= minuteInSeconds -> { | ||
val minutes = (totalSeconds / minuteInSeconds).toInt() | ||
resourceProvider.getQuantityString( | ||
quantity = minutes, | ||
default = R.string.booking_duration_minutes, | ||
one = R.string.booking_duration_minute | ||
) | ||
} | ||
|
||
else -> { | ||
val seconds = totalSeconds.toInt() | ||
resourceProvider.getQuantityString( | ||
quantity = seconds, | ||
default = R.string.booking_duration_seconds, | ||
one = R.string.booking_duration_second | ||
) | ||
} | ||
} | ||
} | ||
|
||
private suspend fun BookingCustomerInfo.address(): Address? { | ||
val countryCode = billingCountry ?: return null | ||
val (country, state) = withContext(Dispatchers.IO) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The normalization logic could be simplified by extracting the boundary checking into a separate function. This would make the algorithm clearer and more testable.
Copilot uses AI. Check for mistakes.