Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/aniso8601/builders/python.py: 90%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

296 statements  

1# -*- coding: utf-8 -*- 

2 

3# Copyright (c) 2026, Brandon Nielsen 

4# SPDX-License-Identifier: BSD-3-Clause 

5 

6import datetime 

7from collections import namedtuple 

8from functools import partial 

9 

10from aniso8601.builders import ( 

11 BaseTimeBuilder, 

12 DateTuple, 

13 Limit, 

14 TupleBuilder, 

15 cast, 

16 range_check, 

17) 

18from aniso8601.exceptions import ( 

19 DayOutOfBoundsError, 

20 HoursOutOfBoundsError, 

21 MinutesOutOfBoundsError, 

22 MonthOutOfBoundsError, 

23 SecondsOutOfBoundsError, 

24 WeekOutOfBoundsError, 

25 YearOutOfBoundsError, 

26) 

27from aniso8601.utcoffset import UTCOffset 

28 

29DAYS_PER_YEAR = 365 

30DAYS_PER_MONTH = 30 

31DAYS_PER_WEEK = 7 

32 

33HOURS_PER_DAY = 24 

34 

35MINUTES_PER_HOUR = 60 

36MINUTES_PER_DAY = MINUTES_PER_HOUR * HOURS_PER_DAY 

37 

38SECONDS_PER_MINUTE = 60 

39SECONDS_PER_DAY = MINUTES_PER_DAY * SECONDS_PER_MINUTE 

40 

41MICROSECONDS_PER_SECOND = int(1e6) 

42 

43MICROSECONDS_PER_MINUTE = 60 * MICROSECONDS_PER_SECOND 

44MICROSECONDS_PER_HOUR = 60 * MICROSECONDS_PER_MINUTE 

45MICROSECONDS_PER_DAY = 24 * MICROSECONDS_PER_HOUR 

46MICROSECONDS_PER_WEEK = 7 * MICROSECONDS_PER_DAY 

47MICROSECONDS_PER_MONTH = DAYS_PER_MONTH * MICROSECONDS_PER_DAY 

48MICROSECONDS_PER_YEAR = DAYS_PER_YEAR * MICROSECONDS_PER_DAY 

49 

50TIMEDELTA_MAX_DAYS = datetime.timedelta.max.days 

51 

52FractionalComponent = namedtuple( 

53 "FractionalComponent", ["principal", "microsecondremainder"] 

54) 

55 

56 

57def year_range_check(valuestr, limit): 

58 YYYYstr = valuestr 

59 

60 # Truncated dates, like '19', refer to 1900-1999 inclusive, 

61 # we simply parse to 1900, Y and YYY strings are not supported 

62 if len(valuestr) == 2: 

63 # Shift 0s in from the left to form complete year 

64 YYYYstr = valuestr.ljust(4, "0") 

65 

66 return range_check(YYYYstr, limit) 

67 

68 

69def fractional_range_check(conversion, valuestr, limit): 

70 if valuestr is None: 

71 return None 

72 

73 if "." in valuestr: 

74 castfunc = partial(_cast_to_fractional_component, conversion) 

75 else: 

76 castfunc = int 

77 

78 value = cast(valuestr, castfunc, thrownmessage=limit.casterrorstring) 

79 

80 if isinstance(value, FractionalComponent): 

81 tocheck = float(valuestr) 

82 else: 

83 tocheck = int(valuestr) 

84 

85 if limit.min is not None and tocheck < limit.min: 

86 raise limit.rangeexception(limit.rangeerrorstring) 

87 

88 if limit.max is not None and tocheck > limit.max: 

89 raise limit.rangeexception(limit.rangeerrorstring) 

90 

91 return value 

92 

93 

94def _cast_to_fractional_component(conversion, floatstr): 

95 # Splits a string with a decimal point into an int, and 

96 # int representing the floating point remainder as a number 

97 # of microseconds, determined by multiplying by conversion 

98 intpart, floatpart = floatstr.split(".") 

99 

100 intvalue = int(intpart) 

101 preconvertedvalue = int(floatpart) 

102 

103 convertedvalue = (preconvertedvalue * conversion) // (10 ** len(floatpart)) 

104 

105 return FractionalComponent(intvalue, convertedvalue) 

106 

107 

108class PythonTimeBuilder(BaseTimeBuilder): 

109 # 0000 (1 BC) is not representable as a Python date 

110 DATE_YYYY_LIMIT = Limit( 

111 "Invalid year string.", 

112 datetime.MINYEAR, 

113 datetime.MAXYEAR, 

114 YearOutOfBoundsError, 

115 "Year must be between {0}..{1}.".format(datetime.MINYEAR, datetime.MAXYEAR), 

116 year_range_check, 

117 ) 

118 TIME_HH_LIMIT = Limit( 

119 "Invalid hour string.", 

120 0, 

121 24, 

122 HoursOutOfBoundsError, 

123 "Hour must be between 0..24 with 24 representing midnight.", 

124 partial(fractional_range_check, MICROSECONDS_PER_HOUR), 

125 ) 

126 TIME_MM_LIMIT = Limit( 

127 "Invalid minute string.", 

128 0, 

129 59, 

130 MinutesOutOfBoundsError, 

131 "Minute must be between 0..59.", 

132 partial(fractional_range_check, MICROSECONDS_PER_MINUTE), 

133 ) 

134 TIME_SS_LIMIT = Limit( 

135 "Invalid second string.", 

136 0, 

137 60, 

138 SecondsOutOfBoundsError, 

139 "Second must be between 0..60 with 60 representing a leap second.", 

140 partial(fractional_range_check, MICROSECONDS_PER_SECOND), 

141 ) 

142 DURATION_PNY_LIMIT = Limit( 

143 "Invalid year duration string.", 

144 None, 

145 None, 

146 YearOutOfBoundsError, 

147 None, 

148 partial(fractional_range_check, MICROSECONDS_PER_YEAR), 

149 ) 

150 DURATION_PNM_LIMIT = Limit( 

151 "Invalid month duration string.", 

152 None, 

153 None, 

154 MonthOutOfBoundsError, 

155 None, 

156 partial(fractional_range_check, MICROSECONDS_PER_MONTH), 

157 ) 

158 DURATION_PNW_LIMIT = Limit( 

159 "Invalid week duration string.", 

160 None, 

161 None, 

162 WeekOutOfBoundsError, 

163 None, 

164 partial(fractional_range_check, MICROSECONDS_PER_WEEK), 

165 ) 

166 DURATION_PND_LIMIT = Limit( 

167 "Invalid day duration string.", 

168 None, 

169 None, 

170 DayOutOfBoundsError, 

171 None, 

172 partial(fractional_range_check, MICROSECONDS_PER_DAY), 

173 ) 

174 DURATION_TNH_LIMIT = Limit( 

175 "Invalid hour duration string.", 

176 None, 

177 None, 

178 HoursOutOfBoundsError, 

179 None, 

180 partial(fractional_range_check, MICROSECONDS_PER_HOUR), 

181 ) 

182 DURATION_TNM_LIMIT = Limit( 

183 "Invalid minute duration string.", 

184 None, 

185 None, 

186 MinutesOutOfBoundsError, 

187 None, 

188 partial(fractional_range_check, MICROSECONDS_PER_MINUTE), 

189 ) 

190 DURATION_TNS_LIMIT = Limit( 

191 "Invalid second duration string.", 

192 None, 

193 None, 

194 SecondsOutOfBoundsError, 

195 None, 

196 partial(fractional_range_check, MICROSECONDS_PER_SECOND), 

197 ) 

198 

199 DATE_RANGE_DICT = BaseTimeBuilder.DATE_RANGE_DICT 

200 DATE_RANGE_DICT["YYYY"] = DATE_YYYY_LIMIT 

201 

202 TIME_RANGE_DICT = {"hh": TIME_HH_LIMIT, "mm": TIME_MM_LIMIT, "ss": TIME_SS_LIMIT} 

203 

204 DURATION_RANGE_DICT = { 

205 "PnY": DURATION_PNY_LIMIT, 

206 "PnM": DURATION_PNM_LIMIT, 

207 "PnW": DURATION_PNW_LIMIT, 

208 "PnD": DURATION_PND_LIMIT, 

209 "TnH": DURATION_TNH_LIMIT, 

210 "TnM": DURATION_TNM_LIMIT, 

211 "TnS": DURATION_TNS_LIMIT, 

212 } 

213 

214 @classmethod 

215 def build_date(cls, YYYY=None, MM=None, DD=None, Www=None, D=None, DDD=None): 

216 YYYY, MM, DD, Www, D, DDD = cls.range_check_date(YYYY, MM, DD, Www, D, DDD) 

217 

218 if MM is None: 

219 MM = 1 

220 

221 if DD is None: 

222 DD = 1 

223 

224 if DDD is not None: 

225 return PythonTimeBuilder._build_ordinal_date(YYYY, DDD) 

226 

227 if Www is not None: 

228 return PythonTimeBuilder._build_week_date(YYYY, Www, isoday=D) 

229 

230 return datetime.date(YYYY, MM, DD) 

231 

232 @classmethod 

233 def build_time(cls, hh=None, mm=None, ss=None, tz=None): 

234 # Builds a time from the given parts, handling fractional arguments 

235 # where necessary 

236 hours = 0 

237 minutes = 0 

238 seconds = 0 

239 microseconds = 0 

240 

241 hh, mm, ss, tz = cls.range_check_time(hh, mm, ss, tz) 

242 

243 if isinstance(hh, FractionalComponent): 

244 hours = hh.principal 

245 microseconds = hh.microsecondremainder 

246 elif hh is not None: 

247 hours = hh 

248 

249 if isinstance(mm, FractionalComponent): 

250 minutes = mm.principal 

251 microseconds = mm.microsecondremainder 

252 elif mm is not None: 

253 minutes = mm 

254 

255 if isinstance(ss, FractionalComponent): 

256 seconds = ss.principal 

257 microseconds = ss.microsecondremainder 

258 elif ss is not None: 

259 seconds = ss 

260 

261 ( 

262 hours, 

263 minutes, 

264 seconds, 

265 microseconds, 

266 ) = PythonTimeBuilder._distribute_microseconds( 

267 microseconds, 

268 (hours, minutes, seconds), 

269 (MICROSECONDS_PER_HOUR, MICROSECONDS_PER_MINUTE, MICROSECONDS_PER_SECOND), 

270 ) 

271 

272 # Move midnight into range 

273 if hours == 24: 

274 hours = 0 

275 

276 # Datetimes don't handle fractional components, so we use a timedelta 

277 if tz is not None: 

278 return ( 

279 datetime.datetime( 

280 1, 1, 1, hour=hours, minute=minutes, tzinfo=cls._build_object(tz) 

281 ) 

282 + datetime.timedelta(seconds=seconds, microseconds=microseconds) 

283 ).timetz() 

284 

285 return ( 

286 datetime.datetime(1, 1, 1, hour=hours, minute=minutes) 

287 + datetime.timedelta(seconds=seconds, microseconds=microseconds) 

288 ).time() 

289 

290 @classmethod 

291 def build_datetime(cls, date, time): 

292 return datetime.datetime.combine( 

293 cls._build_object(date), cls._build_object(time) 

294 ) 

295 

296 @classmethod 

297 def build_duration( 

298 cls, PnY=None, PnM=None, PnW=None, PnD=None, TnH=None, TnM=None, TnS=None 

299 ): 

300 # PnY and PnM will be distributed to PnD, microsecond remainder to TnS 

301 PnY, PnM, PnW, PnD, TnH, TnM, TnS = cls.range_check_duration( 

302 PnY, PnM, PnW, PnD, TnH, TnM, TnS 

303 ) 

304 

305 seconds = TnS.principal 

306 microseconds = TnS.microsecondremainder 

307 

308 return datetime.timedelta( 

309 days=PnD, 

310 seconds=seconds, 

311 microseconds=microseconds, 

312 minutes=TnM, 

313 hours=TnH, 

314 weeks=PnW, 

315 ) 

316 

317 @classmethod 

318 def build_interval(cls, start=None, end=None, duration=None): 

319 start, end, duration = cls.range_check_interval(start, end, duration) 

320 

321 if start is not None and end is not None: 

322 # <start>/<end> 

323 startobject = cls._build_object(start) 

324 endobject = cls._build_object(end) 

325 

326 return (startobject, endobject) 

327 

328 durationobject = cls._build_object(duration) 

329 

330 # Determine if datetime promotion is required 

331 datetimerequired = ( 

332 duration.TnH is not None 

333 or duration.TnM is not None 

334 or duration.TnS is not None 

335 or durationobject.seconds != 0 

336 or durationobject.microseconds != 0 

337 ) 

338 

339 if end is not None: 

340 # <duration>/<end> 

341 endobject = cls._build_object(end) 

342 

343 # Range check 

344 if isinstance(end, DateTuple) and datetimerequired is True: 

345 # <end> is a date, and <duration> requires datetime resolution 

346 return ( 

347 endobject, 

348 cls.build_datetime(end, TupleBuilder.build_time()) - durationobject, 

349 ) 

350 

351 return (endobject, endobject - durationobject) 

352 

353 # <start>/<duration> 

354 startobject = cls._build_object(start) 

355 

356 # Range check 

357 if isinstance(start, DateTuple) and datetimerequired is True: 

358 # <start> is a date, and <duration> requires datetime resolution 

359 return ( 

360 startobject, 

361 cls.build_datetime(start, TupleBuilder.build_time()) + durationobject, 

362 ) 

363 

364 return (startobject, startobject + durationobject) 

365 

366 @classmethod 

367 def build_repeating_interval(cls, R=None, Rnn=None, interval=None): 

368 startobject = None 

369 endobject = None 

370 

371 R, Rnn, interval = cls.range_check_repeating_interval(R, Rnn, interval) 

372 

373 if interval.start is not None: 

374 startobject = cls._build_object(interval.start) 

375 

376 if interval.end is not None: 

377 endobject = cls._build_object(interval.end) 

378 

379 if interval.duration is not None: 

380 durationobject = cls._build_object(interval.duration) 

381 else: 

382 durationobject = endobject - startobject 

383 

384 if R is True: 

385 if startobject is not None: 

386 return cls._date_generator_unbounded(startobject, durationobject) 

387 

388 return cls._date_generator_unbounded(endobject, -durationobject) 

389 

390 iterations = int(Rnn) 

391 

392 if startobject is not None: 

393 return cls._date_generator(startobject, durationobject, iterations) 

394 

395 return cls._date_generator(endobject, -durationobject, iterations) 

396 

397 @classmethod 

398 def build_timezone(cls, negative=None, Z=None, hh=None, mm=None, name=""): 

399 negative, Z, hh, mm, name = cls.range_check_timezone(negative, Z, hh, mm, name) 

400 

401 if Z is True: 

402 # Z -> UTC 

403 return UTCOffset(name="UTC", minutes=0) 

404 

405 tzhour = int(hh) 

406 

407 if mm is not None: 

408 tzminute = int(mm) 

409 else: 

410 tzminute = 0 

411 

412 if negative is True: 

413 return UTCOffset(name=name, minutes=-(tzhour * 60 + tzminute)) 

414 

415 return UTCOffset(name=name, minutes=tzhour * 60 + tzminute) 

416 

417 @classmethod 

418 def range_check_duration( 

419 cls, 

420 PnY=None, 

421 PnM=None, 

422 PnW=None, 

423 PnD=None, 

424 TnH=None, 

425 TnM=None, 

426 TnS=None, 

427 rangedict=None, 

428 ): 

429 years = 0 

430 months = 0 

431 days = 0 

432 weeks = 0 

433 hours = 0 

434 minutes = 0 

435 seconds = 0 

436 microseconds = 0 

437 

438 PnY, PnM, PnW, PnD, TnH, TnM, TnS = BaseTimeBuilder.range_check_duration( 

439 PnY, PnM, PnW, PnD, TnH, TnM, TnS, rangedict=cls.DURATION_RANGE_DICT 

440 ) 

441 

442 if PnY is not None: 

443 if isinstance(PnY, FractionalComponent): 

444 years = PnY.principal 

445 microseconds = PnY.microsecondremainder 

446 else: 

447 years = PnY 

448 

449 if years * DAYS_PER_YEAR > TIMEDELTA_MAX_DAYS: 

450 raise YearOutOfBoundsError("Duration exceeds maximum timedelta size.") 

451 

452 if PnM is not None: 

453 if isinstance(PnM, FractionalComponent): 

454 months = PnM.principal 

455 microseconds = PnM.microsecondremainder 

456 else: 

457 months = PnM 

458 

459 if months * DAYS_PER_MONTH > TIMEDELTA_MAX_DAYS: 

460 raise MonthOutOfBoundsError("Duration exceeds maximum timedelta size.") 

461 

462 if PnW is not None: 

463 if isinstance(PnW, FractionalComponent): 

464 weeks = PnW.principal 

465 microseconds = PnW.microsecondremainder 

466 else: 

467 weeks = PnW 

468 

469 if weeks * DAYS_PER_WEEK > TIMEDELTA_MAX_DAYS: 

470 raise WeekOutOfBoundsError("Duration exceeds maximum timedelta size.") 

471 

472 if PnD is not None: 

473 if isinstance(PnD, FractionalComponent): 

474 days = PnD.principal 

475 microseconds = PnD.microsecondremainder 

476 else: 

477 days = PnD 

478 

479 if days > TIMEDELTA_MAX_DAYS: 

480 raise DayOutOfBoundsError("Duration exceeds maximum timedelta size.") 

481 

482 if TnH is not None: 

483 if isinstance(TnH, FractionalComponent): 

484 hours = TnH.principal 

485 microseconds = TnH.microsecondremainder 

486 else: 

487 hours = TnH 

488 

489 if hours // HOURS_PER_DAY > TIMEDELTA_MAX_DAYS: 

490 raise HoursOutOfBoundsError("Duration exceeds maximum timedelta size.") 

491 

492 if TnM is not None: 

493 if isinstance(TnM, FractionalComponent): 

494 minutes = TnM.principal 

495 microseconds = TnM.microsecondremainder 

496 else: 

497 minutes = TnM 

498 

499 if minutes // MINUTES_PER_DAY > TIMEDELTA_MAX_DAYS: 

500 raise MinutesOutOfBoundsError( 

501 "Duration exceeds maximum timedelta size." 

502 ) 

503 

504 if TnS is not None: 

505 if isinstance(TnS, FractionalComponent): 

506 seconds = TnS.principal 

507 microseconds = TnS.microsecondremainder 

508 else: 

509 seconds = TnS 

510 

511 if seconds // SECONDS_PER_DAY > TIMEDELTA_MAX_DAYS: 

512 raise SecondsOutOfBoundsError( 

513 "Duration exceeds maximum timedelta size." 

514 ) 

515 

516 ( 

517 years, 

518 months, 

519 weeks, 

520 days, 

521 hours, 

522 minutes, 

523 seconds, 

524 microseconds, 

525 ) = PythonTimeBuilder._distribute_microseconds( 

526 microseconds, 

527 (years, months, weeks, days, hours, minutes, seconds), 

528 ( 

529 MICROSECONDS_PER_YEAR, 

530 MICROSECONDS_PER_MONTH, 

531 MICROSECONDS_PER_WEEK, 

532 MICROSECONDS_PER_DAY, 

533 MICROSECONDS_PER_HOUR, 

534 MICROSECONDS_PER_MINUTE, 

535 MICROSECONDS_PER_SECOND, 

536 ), 

537 ) 

538 

539 # Note that weeks can be handled without conversion to days 

540 totaldays = years * DAYS_PER_YEAR + months * DAYS_PER_MONTH + days 

541 

542 # Check against timedelta limits 

543 if ( 

544 totaldays 

545 + weeks * DAYS_PER_WEEK 

546 + hours // HOURS_PER_DAY 

547 + minutes // MINUTES_PER_DAY 

548 + seconds // SECONDS_PER_DAY 

549 > TIMEDELTA_MAX_DAYS 

550 ): 

551 raise DayOutOfBoundsError("Duration exceeds maximum timedelta size.") 

552 

553 return ( 

554 None, 

555 None, 

556 weeks, 

557 totaldays, 

558 hours, 

559 minutes, 

560 FractionalComponent(seconds, microseconds), 

561 ) 

562 

563 @classmethod 

564 def range_check_interval(cls, start=None, end=None, duration=None): 

565 # Handles concise format, range checks any potential durations 

566 if start is not None and end is not None: 

567 # <start>/<end> 

568 # Handle concise format 

569 if cls._is_interval_end_concise(end) is True: 

570 end = cls._combine_concise_interval_tuples(start, end) 

571 

572 return (start, end, duration) 

573 

574 durationobject = cls._build_object(duration) 

575 

576 if end is not None: 

577 # <duration>/<end> 

578 endobject = cls._build_object(end) 

579 

580 # Range check 

581 if isinstance(end, DateTuple): 

582 enddatetime = cls.build_datetime(end, TupleBuilder.build_time()) 

583 

584 if enddatetime - datetime.datetime.min < durationobject: 

585 raise YearOutOfBoundsError("Interval end less than minimium date.") 

586 else: 

587 mindatetime = datetime.datetime.min 

588 

589 if end.time.tz is not None: 

590 mindatetime = mindatetime.replace(tzinfo=endobject.tzinfo) 

591 

592 if endobject - mindatetime < durationobject: 

593 raise YearOutOfBoundsError("Interval end less than minimium date.") 

594 else: 

595 # <start>/<duration> 

596 startobject = cls._build_object(start) 

597 

598 # Range check 

599 if type(start) is DateTuple: 

600 startdatetime = cls.build_datetime(start, TupleBuilder.build_time()) 

601 

602 if datetime.datetime.max - startdatetime < durationobject: 

603 raise YearOutOfBoundsError( 

604 "Interval end greater than maximum date." 

605 ) 

606 else: 

607 maxdatetime = datetime.datetime.max 

608 

609 if start.time.tz is not None: 

610 maxdatetime = maxdatetime.replace(tzinfo=startobject.tzinfo) 

611 

612 if maxdatetime - startobject < durationobject: 

613 raise YearOutOfBoundsError( 

614 "Interval end greater than maximum date." 

615 ) 

616 

617 return (start, end, duration) 

618 

619 @staticmethod 

620 def _build_week_date(isoyear, isoweek, isoday=None): 

621 if isoday is None: 

622 return PythonTimeBuilder._iso_year_start(isoyear) + datetime.timedelta( 

623 weeks=isoweek - 1 

624 ) 

625 

626 return PythonTimeBuilder._iso_year_start(isoyear) + datetime.timedelta( 

627 weeks=isoweek - 1, days=isoday - 1 

628 ) 

629 

630 @staticmethod 

631 def _build_ordinal_date(isoyear, isoday): 

632 # Day of year to a date 

633 # https://stackoverflow.com/questions/2427555/python-question-year-and-day-of-year-to-date 

634 builtdate = datetime.date(isoyear, 1, 1) + datetime.timedelta(days=isoday - 1) 

635 

636 return builtdate 

637 

638 @staticmethod 

639 def _iso_year_start(isoyear): 

640 # Given an ISO year, returns the equivalent of the start of the year 

641 # on the Gregorian calendar (which is used by Python) 

642 # Stolen from: 

643 # http://stackoverflow.com/questions/304256/whats-the-best-way-to-find-the-inverse-of-datetime-isocalendar 

644 

645 # Determine the location of the 4th of January, the first week of 

646 # the ISO year is the week containing the 4th of January 

647 # http://en.wikipedia.org/wiki/ISO_week_date 

648 fourth_jan = datetime.date(isoyear, 1, 4) 

649 

650 # Note the conversion from ISO day (1 - 7) and Python day (0 - 6) 

651 delta = datetime.timedelta(days=fourth_jan.isoweekday() - 1) 

652 

653 # Return the start of the year 

654 return fourth_jan - delta 

655 

656 @staticmethod 

657 def _date_generator(startdate, timedelta, iterations): 

658 currentdate = startdate 

659 currentiteration = 0 

660 

661 while currentiteration < iterations: 

662 yield currentdate 

663 

664 # Update the values 

665 currentdate += timedelta 

666 currentiteration += 1 

667 

668 @staticmethod 

669 def _date_generator_unbounded(startdate, timedelta): 

670 currentdate = startdate 

671 

672 while True: 

673 yield currentdate 

674 

675 # Update the value 

676 currentdate += timedelta 

677 

678 @staticmethod 

679 def _distribute_microseconds(todistribute, recipients, reductions): 

680 # Given a number of microseconds as int, a tuple of ints length n 

681 # to distribute to, and a tuple of ints length n to divide todistribute 

682 # by (from largest to smallest), returns a tuple of length n + 1, with 

683 # todistribute divided across recipients using the reductions, with 

684 # the final remainder returned as the final tuple member 

685 results = [] 

686 

687 remainder = todistribute 

688 

689 for index, reduction in enumerate(reductions): 

690 additional, remainder = divmod(remainder, reduction) 

691 

692 results.append(recipients[index] + additional) 

693 

694 # Always return the remaining microseconds 

695 results.append(remainder) 

696 

697 return tuple(results)