@@ -15,6 +15,7 @@ import com.woocommerce.android.ui.bookings.compose.BookingSummaryModel
1515import com.woocommerce.android.ui.bookings.details.CancelStatus
1616import com.woocommerce.android.ui.bookings.list.BookingListItem
1717import com.woocommerce.android.util.CurrencyFormatter
18+ import com.woocommerce.android.viewmodel.ResourceProvider
1819import kotlinx.coroutines.Dispatchers
1920import kotlinx.coroutines.withContext
2021import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingCustomerInfo
@@ -29,7 +30,8 @@ import javax.inject.Inject
2930
3031class BookingMapper @Inject constructor(
3132 private val currencyFormatter : CurrencyFormatter ,
32- private val getLocations : GetLocations
33+ private val getLocations : GetLocations ,
34+ private val resourceProvider : ResourceProvider
3335) {
3436 private val summaryDateFormatter: DateTimeFormatter = DateTimeFormatter .ofLocalizedDateTime(
3537 FormatStyle .MEDIUM ,
@@ -62,16 +64,18 @@ class BookingMapper @Inject constructor(
6264 staffMemberStatus : BookingStaffMemberStatus ? ,
6365 cancelStatus : CancelStatus ,
6466 ): BookingAppointmentDetailsModel {
65- val durationMinutes = Duration .between(start, end).toMinutes()
67+ val duration = Duration .between(start, end)
68+ .normalizeBookingDuration()
69+ .toHumanReadableFormat()
6670 return BookingAppointmentDetailsModel (
6771 date = detailsDateFormatter.format(start),
6872 time = " ${timeRangeFormatter.format(start)} - ${timeRangeFormatter.format(end)} " ,
6973 staff = staffMemberStatus,
7074 // TODO replace mocked values when available from API
7175 location = " 238 Willow Creek Drive, Montgomery AL 36109" ,
72- duration = " $durationMinutes min" ,
7376 price = currencyFormatter.formatCurrency(cost, currency),
7477 cancelStatus = cancelStatus,
78+ duration = duration,
7579 )
7680 }
7781
@@ -131,6 +135,110 @@ class BookingMapper @Inject constructor(
131135 )
132136 }
133137
138+ /* *
139+ * Normalize booking duration by adjusting for precision issues.
140+ *
141+ * This function handles cases where a booking duration is very close to
142+ * common time boundaries (days/hours) but falls short due to precision issues.
143+ * It rounds up durations that are within one minute of these boundaries.
144+ */
145+ private fun Duration.normalizeBookingDuration (): Duration {
146+ val dayInSeconds = Duration .ofDays(1 ).seconds
147+ val hourInSeconds = Duration .ofHours(1 ).seconds
148+ val minuteInSeconds = Duration .ofMinutes(1 ).seconds
149+
150+ var durationInSeconds = this .seconds
151+ val boundaries = listOf (dayInSeconds, hourInSeconds)
152+ for (boundary in boundaries) {
153+ val remainder = durationInSeconds % boundary
154+ val difference = if (remainder == 0L ) 0L else boundary - remainder
155+ if (difference > 0 && difference <= minuteInSeconds) {
156+ durationInSeconds + = difference
157+ }
158+ }
159+ return Duration .ofSeconds(durationInSeconds)
160+ }
161+
162+ @Suppress(" LongMethod" )
163+ private fun Duration.toHumanReadableFormat (): String {
164+ val totalSeconds = seconds
165+ val dayInSeconds = Duration .ofDays(1 ).toSeconds()
166+ val hourInSeconds = Duration .ofHours(1 ).toSeconds()
167+ val minuteInSeconds = Duration .ofMinutes(1 ).toSeconds()
168+
169+ return when {
170+ totalSeconds >= dayInSeconds -> {
171+ val days = (totalSeconds / dayInSeconds).toInt()
172+ val hours = ((totalSeconds % dayInSeconds) / hourInSeconds).toInt()
173+ val minutes = ((totalSeconds % hourInSeconds) / minuteInSeconds).toInt()
174+
175+ val parts = mutableListOf<String >()
176+ parts + = resourceProvider.getQuantityString(
177+ quantity = days,
178+ default = R .string.booking_duration_days,
179+ one = R .string.booking_duration_day
180+ )
181+ if (hours > 0 ) {
182+ parts + = resourceProvider.getQuantityString(
183+ quantity = hours,
184+ default = R .string.booking_duration_hours,
185+ one = R .string.booking_duration_hour
186+ )
187+ }
188+ if (minutes > 0 ) {
189+ parts + = resourceProvider.getQuantityString(
190+ quantity = minutes,
191+ default = R .string.booking_duration_minutes,
192+ one = R .string.booking_duration_minute
193+ )
194+ }
195+ parts.joinToString(separator = " " )
196+ }
197+
198+ totalSeconds >= hourInSeconds -> {
199+ val hours = (totalSeconds / hourInSeconds).toInt()
200+ val minutes = ((totalSeconds % hourInSeconds) / minuteInSeconds).toInt()
201+ if (minutes == 0 ) {
202+ resourceProvider.getQuantityString(
203+ quantity = hours,
204+ default = R .string.booking_duration_hours,
205+ one = R .string.booking_duration_hour
206+ )
207+ } else {
208+ val hoursPart = resourceProvider.getQuantityString(
209+ quantity = hours,
210+ default = R .string.booking_duration_hours,
211+ one = R .string.booking_duration_hour
212+ )
213+ val minutesPart = resourceProvider.getQuantityString(
214+ quantity = minutes,
215+ default = R .string.booking_duration_minutes,
216+ one = R .string.booking_duration_minute
217+ )
218+ " $hoursPart $minutesPart "
219+ }
220+ }
221+
222+ totalSeconds >= minuteInSeconds -> {
223+ val minutes = (totalSeconds / minuteInSeconds).toInt()
224+ resourceProvider.getQuantityString(
225+ quantity = minutes,
226+ default = R .string.booking_duration_minutes,
227+ one = R .string.booking_duration_minute
228+ )
229+ }
230+
231+ else -> {
232+ val seconds = totalSeconds.toInt()
233+ resourceProvider.getQuantityString(
234+ quantity = seconds,
235+ default = R .string.booking_duration_seconds,
236+ one = R .string.booking_duration_second
237+ )
238+ }
239+ }
240+ }
241+
134242 private suspend fun BookingCustomerInfo.address (): Address ? {
135243 val countryCode = billingCountry ? : return null
136244 val (country, state) = withContext(Dispatchers .IO ) {
0 commit comments