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

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) 2025, Brandon Nielsen 

4# All rights reserved. 

5# 

6# This software may be modified and distributed under the terms 

7# of the BSD license. See the LICENSE file for details. 

8 

9import datetime 

10from collections import namedtuple 

11from functools import partial 

12 

13from aniso8601.builders import ( 

14 BaseTimeBuilder, 

15 DateTuple, 

16 Limit, 

17 TupleBuilder, 

18 cast, 

19 range_check, 

20) 

21from aniso8601.exceptions import ( 

22 DayOutOfBoundsError, 

23 HoursOutOfBoundsError, 

24 MinutesOutOfBoundsError, 

25 MonthOutOfBoundsError, 

26 SecondsOutOfBoundsError, 

27 WeekOutOfBoundsError, 

28 YearOutOfBoundsError, 

29) 

30from aniso8601.utcoffset import UTCOffset 

31 

32DAYS_PER_YEAR = 365 

33DAYS_PER_MONTH = 30 

34DAYS_PER_WEEK = 7 

35 

36HOURS_PER_DAY = 24 

37 

38MINUTES_PER_HOUR = 60 

39MINUTES_PER_DAY = MINUTES_PER_HOUR * HOURS_PER_DAY 

40 

41SECONDS_PER_MINUTE = 60 

42SECONDS_PER_DAY = MINUTES_PER_DAY * SECONDS_PER_MINUTE 

43 

44MICROSECONDS_PER_SECOND = int(1e6) 

45 

46MICROSECONDS_PER_MINUTE = 60 * MICROSECONDS_PER_SECOND 

47MICROSECONDS_PER_HOUR = 60 * MICROSECONDS_PER_MINUTE 

48MICROSECONDS_PER_DAY = 24 * MICROSECONDS_PER_HOUR 

49MICROSECONDS_PER_WEEK = 7 * MICROSECONDS_PER_DAY 

50MICROSECONDS_PER_MONTH = DAYS_PER_MONTH * MICROSECONDS_PER_DAY 

51MICROSECONDS_PER_YEAR = DAYS_PER_YEAR * MICROSECONDS_PER_DAY 

52 

53TIMEDELTA_MAX_DAYS = datetime.timedelta.max.days 

54 

55FractionalComponent = namedtuple( 

56 "FractionalComponent", ["principal", "microsecondremainder"] 

57) 

58 

59 

60def year_range_check(valuestr, limit): 

61 YYYYstr = valuestr 

62 

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

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

65 if len(valuestr) == 2: 

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

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

68 

69 return range_check(YYYYstr, limit) 

70 

71 

72def fractional_range_check(conversion, valuestr, limit): 

73 if valuestr is None: 

74 return None 

75 

76 if "." in valuestr: 

77 castfunc = partial(_cast_to_fractional_component, conversion) 

78 else: 

79 castfunc = int 

80 

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

82 

83 if isinstance(value, FractionalComponent): 

84 tocheck = float(valuestr) 

85 else: 

86 tocheck = int(valuestr) 

87 

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

89 raise limit.rangeexception(limit.rangeerrorstring) 

90 

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

92 raise limit.rangeexception(limit.rangeerrorstring) 

93 

94 return value 

95 

96 

97def _cast_to_fractional_component(conversion, floatstr): 

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

99 # int representing the floating point remainder as a number 

100 # of microseconds, determined by multiplying by conversion 

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

102 

103 intvalue = int(intpart) 

104 preconvertedvalue = int(floatpart) 

105 

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

107 

108 return FractionalComponent(intvalue, convertedvalue) 

109 

110 

111class PythonTimeBuilder(BaseTimeBuilder): 

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

113 DATE_YYYY_LIMIT = Limit( 

114 "Invalid year string.", 

115 datetime.MINYEAR, 

116 datetime.MAXYEAR, 

117 YearOutOfBoundsError, 

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

119 year_range_check, 

120 ) 

121 TIME_HH_LIMIT = Limit( 

122 "Invalid hour string.", 

123 0, 

124 24, 

125 HoursOutOfBoundsError, 

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

127 partial(fractional_range_check, MICROSECONDS_PER_HOUR), 

128 ) 

129 TIME_MM_LIMIT = Limit( 

130 "Invalid minute string.", 

131 0, 

132 59, 

133 MinutesOutOfBoundsError, 

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

135 partial(fractional_range_check, MICROSECONDS_PER_MINUTE), 

136 ) 

137 TIME_SS_LIMIT = Limit( 

138 "Invalid second string.", 

139 0, 

140 60, 

141 SecondsOutOfBoundsError, 

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

143 partial(fractional_range_check, MICROSECONDS_PER_SECOND), 

144 ) 

145 DURATION_PNY_LIMIT = Limit( 

146 "Invalid year duration string.", 

147 None, 

148 None, 

149 YearOutOfBoundsError, 

150 None, 

151 partial(fractional_range_check, MICROSECONDS_PER_YEAR), 

152 ) 

153 DURATION_PNM_LIMIT = Limit( 

154 "Invalid month duration string.", 

155 None, 

156 None, 

157 MonthOutOfBoundsError, 

158 None, 

159 partial(fractional_range_check, MICROSECONDS_PER_MONTH), 

160 ) 

161 DURATION_PNW_LIMIT = Limit( 

162 "Invalid week duration string.", 

163 None, 

164 None, 

165 WeekOutOfBoundsError, 

166 None, 

167 partial(fractional_range_check, MICROSECONDS_PER_WEEK), 

168 ) 

169 DURATION_PND_LIMIT = Limit( 

170 "Invalid day duration string.", 

171 None, 

172 None, 

173 DayOutOfBoundsError, 

174 None, 

175 partial(fractional_range_check, MICROSECONDS_PER_DAY), 

176 ) 

177 DURATION_TNH_LIMIT = Limit( 

178 "Invalid hour duration string.", 

179 None, 

180 None, 

181 HoursOutOfBoundsError, 

182 None, 

183 partial(fractional_range_check, MICROSECONDS_PER_HOUR), 

184 ) 

185 DURATION_TNM_LIMIT = Limit( 

186 "Invalid minute duration string.", 

187 None, 

188 None, 

189 MinutesOutOfBoundsError, 

190 None, 

191 partial(fractional_range_check, MICROSECONDS_PER_MINUTE), 

192 ) 

193 DURATION_TNS_LIMIT = Limit( 

194 "Invalid second duration string.", 

195 None, 

196 None, 

197 SecondsOutOfBoundsError, 

198 None, 

199 partial(fractional_range_check, MICROSECONDS_PER_SECOND), 

200 ) 

201 

202 DATE_RANGE_DICT = BaseTimeBuilder.DATE_RANGE_DICT 

203 DATE_RANGE_DICT["YYYY"] = DATE_YYYY_LIMIT 

204 

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

206 

207 DURATION_RANGE_DICT = { 

208 "PnY": DURATION_PNY_LIMIT, 

209 "PnM": DURATION_PNM_LIMIT, 

210 "PnW": DURATION_PNW_LIMIT, 

211 "PnD": DURATION_PND_LIMIT, 

212 "TnH": DURATION_TNH_LIMIT, 

213 "TnM": DURATION_TNM_LIMIT, 

214 "TnS": DURATION_TNS_LIMIT, 

215 } 

216 

217 @classmethod 

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

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

220 

221 if MM is None: 

222 MM = 1 

223 

224 if DD is None: 

225 DD = 1 

226 

227 if DDD is not None: 

228 return PythonTimeBuilder._build_ordinal_date(YYYY, DDD) 

229 

230 if Www is not None: 

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

232 

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

234 

235 @classmethod 

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

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

238 # where necessary 

239 hours = 0 

240 minutes = 0 

241 seconds = 0 

242 microseconds = 0 

243 

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

245 

246 if isinstance(hh, FractionalComponent): 

247 hours = hh.principal 

248 microseconds = hh.microsecondremainder 

249 elif hh is not None: 

250 hours = hh 

251 

252 if isinstance(mm, FractionalComponent): 

253 minutes = mm.principal 

254 microseconds = mm.microsecondremainder 

255 elif mm is not None: 

256 minutes = mm 

257 

258 if isinstance(ss, FractionalComponent): 

259 seconds = ss.principal 

260 microseconds = ss.microsecondremainder 

261 elif ss is not None: 

262 seconds = ss 

263 

264 ( 

265 hours, 

266 minutes, 

267 seconds, 

268 microseconds, 

269 ) = PythonTimeBuilder._distribute_microseconds( 

270 microseconds, 

271 (hours, minutes, seconds), 

272 (MICROSECONDS_PER_HOUR, MICROSECONDS_PER_MINUTE, MICROSECONDS_PER_SECOND), 

273 ) 

274 

275 # Move midnight into range 

276 if hours == 24: 

277 hours = 0 

278 

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

280 if tz is not None: 

281 return ( 

282 datetime.datetime( 

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

284 ) 

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

286 ).timetz() 

287 

288 return ( 

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

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

291 ).time() 

292 

293 @classmethod 

294 def build_datetime(cls, date, time): 

295 return datetime.datetime.combine( 

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

297 ) 

298 

299 @classmethod 

300 def build_duration( 

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

302 ): 

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

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

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

306 ) 

307 

308 seconds = TnS.principal 

309 microseconds = TnS.microsecondremainder 

310 

311 return datetime.timedelta( 

312 days=PnD, 

313 seconds=seconds, 

314 microseconds=microseconds, 

315 minutes=TnM, 

316 hours=TnH, 

317 weeks=PnW, 

318 ) 

319 

320 @classmethod 

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

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

323 

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

325 # <start>/<end> 

326 startobject = cls._build_object(start) 

327 endobject = cls._build_object(end) 

328 

329 return (startobject, endobject) 

330 

331 durationobject = cls._build_object(duration) 

332 

333 # Determine if datetime promotion is required 

334 datetimerequired = ( 

335 duration.TnH is not None 

336 or duration.TnM is not None 

337 or duration.TnS is not None 

338 or durationobject.seconds != 0 

339 or durationobject.microseconds != 0 

340 ) 

341 

342 if end is not None: 

343 # <duration>/<end> 

344 endobject = cls._build_object(end) 

345 

346 # Range check 

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

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

349 return ( 

350 endobject, 

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

352 ) 

353 

354 return (endobject, endobject - durationobject) 

355 

356 # <start>/<duration> 

357 startobject = cls._build_object(start) 

358 

359 # Range check 

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

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

362 return ( 

363 startobject, 

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

365 ) 

366 

367 return (startobject, startobject + durationobject) 

368 

369 @classmethod 

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

371 startobject = None 

372 endobject = None 

373 

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

375 

376 if interval.start is not None: 

377 startobject = cls._build_object(interval.start) 

378 

379 if interval.end is not None: 

380 endobject = cls._build_object(interval.end) 

381 

382 if interval.duration is not None: 

383 durationobject = cls._build_object(interval.duration) 

384 else: 

385 durationobject = endobject - startobject 

386 

387 if R is True: 

388 if startobject is not None: 

389 return cls._date_generator_unbounded(startobject, durationobject) 

390 

391 return cls._date_generator_unbounded(endobject, -durationobject) 

392 

393 iterations = int(Rnn) 

394 

395 if startobject is not None: 

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

397 

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

399 

400 @classmethod 

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

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

403 

404 if Z is True: 

405 # Z -> UTC 

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

407 

408 tzhour = int(hh) 

409 

410 if mm is not None: 

411 tzminute = int(mm) 

412 else: 

413 tzminute = 0 

414 

415 if negative is True: 

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

417 

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

419 

420 @classmethod 

421 def range_check_duration( 

422 cls, 

423 PnY=None, 

424 PnM=None, 

425 PnW=None, 

426 PnD=None, 

427 TnH=None, 

428 TnM=None, 

429 TnS=None, 

430 rangedict=None, 

431 ): 

432 years = 0 

433 months = 0 

434 days = 0 

435 weeks = 0 

436 hours = 0 

437 minutes = 0 

438 seconds = 0 

439 microseconds = 0 

440 

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

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

443 ) 

444 

445 if PnY is not None: 

446 if isinstance(PnY, FractionalComponent): 

447 years = PnY.principal 

448 microseconds = PnY.microsecondremainder 

449 else: 

450 years = PnY 

451 

452 if years * DAYS_PER_YEAR > TIMEDELTA_MAX_DAYS: 

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

454 

455 if PnM is not None: 

456 if isinstance(PnM, FractionalComponent): 

457 months = PnM.principal 

458 microseconds = PnM.microsecondremainder 

459 else: 

460 months = PnM 

461 

462 if months * DAYS_PER_MONTH > TIMEDELTA_MAX_DAYS: 

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

464 

465 if PnW is not None: 

466 if isinstance(PnW, FractionalComponent): 

467 weeks = PnW.principal 

468 microseconds = PnW.microsecondremainder 

469 else: 

470 weeks = PnW 

471 

472 if weeks * DAYS_PER_WEEK > TIMEDELTA_MAX_DAYS: 

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

474 

475 if PnD is not None: 

476 if isinstance(PnD, FractionalComponent): 

477 days = PnD.principal 

478 microseconds = PnD.microsecondremainder 

479 else: 

480 days = PnD 

481 

482 if days > TIMEDELTA_MAX_DAYS: 

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

484 

485 if TnH is not None: 

486 if isinstance(TnH, FractionalComponent): 

487 hours = TnH.principal 

488 microseconds = TnH.microsecondremainder 

489 else: 

490 hours = TnH 

491 

492 if hours // HOURS_PER_DAY > TIMEDELTA_MAX_DAYS: 

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

494 

495 if TnM is not None: 

496 if isinstance(TnM, FractionalComponent): 

497 minutes = TnM.principal 

498 microseconds = TnM.microsecondremainder 

499 else: 

500 minutes = TnM 

501 

502 if minutes // MINUTES_PER_DAY > TIMEDELTA_MAX_DAYS: 

503 raise MinutesOutOfBoundsError( 

504 "Duration exceeds maximum timedelta size." 

505 ) 

506 

507 if TnS is not None: 

508 if isinstance(TnS, FractionalComponent): 

509 seconds = TnS.principal 

510 microseconds = TnS.microsecondremainder 

511 else: 

512 seconds = TnS 

513 

514 if seconds // SECONDS_PER_DAY > TIMEDELTA_MAX_DAYS: 

515 raise SecondsOutOfBoundsError( 

516 "Duration exceeds maximum timedelta size." 

517 ) 

518 

519 ( 

520 years, 

521 months, 

522 weeks, 

523 days, 

524 hours, 

525 minutes, 

526 seconds, 

527 microseconds, 

528 ) = PythonTimeBuilder._distribute_microseconds( 

529 microseconds, 

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

531 ( 

532 MICROSECONDS_PER_YEAR, 

533 MICROSECONDS_PER_MONTH, 

534 MICROSECONDS_PER_WEEK, 

535 MICROSECONDS_PER_DAY, 

536 MICROSECONDS_PER_HOUR, 

537 MICROSECONDS_PER_MINUTE, 

538 MICROSECONDS_PER_SECOND, 

539 ), 

540 ) 

541 

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

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

544 

545 # Check against timedelta limits 

546 if ( 

547 totaldays 

548 + weeks * DAYS_PER_WEEK 

549 + hours // HOURS_PER_DAY 

550 + minutes // MINUTES_PER_DAY 

551 + seconds // SECONDS_PER_DAY 

552 > TIMEDELTA_MAX_DAYS 

553 ): 

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

555 

556 return ( 

557 None, 

558 None, 

559 weeks, 

560 totaldays, 

561 hours, 

562 minutes, 

563 FractionalComponent(seconds, microseconds), 

564 ) 

565 

566 @classmethod 

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

568 # Handles concise format, range checks any potential durations 

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

570 # <start>/<end> 

571 # Handle concise format 

572 if cls._is_interval_end_concise(end) is True: 

573 end = cls._combine_concise_interval_tuples(start, end) 

574 

575 return (start, end, duration) 

576 

577 durationobject = cls._build_object(duration) 

578 

579 if end is not None: 

580 # <duration>/<end> 

581 endobject = cls._build_object(end) 

582 

583 # Range check 

584 if isinstance(end, DateTuple): 

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

586 

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

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

589 else: 

590 mindatetime = datetime.datetime.min 

591 

592 if end.time.tz is not None: 

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

594 

595 if endobject - mindatetime < durationobject: 

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

597 else: 

598 # <start>/<duration> 

599 startobject = cls._build_object(start) 

600 

601 # Range check 

602 if type(start) is DateTuple: 

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

604 

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

606 raise YearOutOfBoundsError( 

607 "Interval end greater than maximum date." 

608 ) 

609 else: 

610 maxdatetime = datetime.datetime.max 

611 

612 if start.time.tz is not None: 

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

614 

615 if maxdatetime - startobject < durationobject: 

616 raise YearOutOfBoundsError( 

617 "Interval end greater than maximum date." 

618 ) 

619 

620 return (start, end, duration) 

621 

622 @staticmethod 

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

624 if isoday is None: 

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

626 weeks=isoweek - 1 

627 ) 

628 

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

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

631 ) 

632 

633 @staticmethod 

634 def _build_ordinal_date(isoyear, isoday): 

635 # Day of year to a date 

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

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

638 

639 return builtdate 

640 

641 @staticmethod 

642 def _iso_year_start(isoyear): 

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

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

645 # Stolen from: 

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

647 

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

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

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

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

652 

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

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

655 

656 # Return the start of the year 

657 return fourth_jan - delta 

658 

659 @staticmethod 

660 def _date_generator(startdate, timedelta, iterations): 

661 currentdate = startdate 

662 currentiteration = 0 

663 

664 while currentiteration < iterations: 

665 yield currentdate 

666 

667 # Update the values 

668 currentdate += timedelta 

669 currentiteration += 1 

670 

671 @staticmethod 

672 def _date_generator_unbounded(startdate, timedelta): 

673 currentdate = startdate 

674 

675 while True: 

676 yield currentdate 

677 

678 # Update the value 

679 currentdate += timedelta 

680 

681 @staticmethod 

682 def _distribute_microseconds(todistribute, recipients, reductions): 

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

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

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

686 # todistribute divided across recipients using the reductions, with 

687 # the final remainder returned as the final tuple member 

688 results = [] 

689 

690 remainder = todistribute 

691 

692 for index, reduction in enumerate(reductions): 

693 additional, remainder = divmod(remainder, reduction) 

694 

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

696 

697 # Always return the remaining microseconds 

698 results.append(remainder) 

699 

700 return tuple(results)