Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/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

294 statements  

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

2 

3# Copyright (c) 2021, 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 DatetimeTuple, 

16 DateTuple, 

17 Limit, 

18 TimeTuple, 

19 TupleBuilder, 

20 cast, 

21 range_check, 

22) 

23from aniso8601.exceptions import ( 

24 DayOutOfBoundsError, 

25 HoursOutOfBoundsError, 

26 ISOFormatError, 

27 LeapSecondError, 

28 MidnightBoundsError, 

29 MinutesOutOfBoundsError, 

30 MonthOutOfBoundsError, 

31 SecondsOutOfBoundsError, 

32 WeekOutOfBoundsError, 

33 YearOutOfBoundsError, 

34) 

35from aniso8601.utcoffset import UTCOffset 

36 

37DAYS_PER_YEAR = 365 

38DAYS_PER_MONTH = 30 

39DAYS_PER_WEEK = 7 

40 

41HOURS_PER_DAY = 24 

42 

43MINUTES_PER_HOUR = 60 

44MINUTES_PER_DAY = MINUTES_PER_HOUR * HOURS_PER_DAY 

45 

46SECONDS_PER_MINUTE = 60 

47SECONDS_PER_DAY = MINUTES_PER_DAY * SECONDS_PER_MINUTE 

48 

49MICROSECONDS_PER_SECOND = int(1e6) 

50 

51MICROSECONDS_PER_MINUTE = 60 * MICROSECONDS_PER_SECOND 

52MICROSECONDS_PER_HOUR = 60 * MICROSECONDS_PER_MINUTE 

53MICROSECONDS_PER_DAY = 24 * MICROSECONDS_PER_HOUR 

54MICROSECONDS_PER_WEEK = 7 * MICROSECONDS_PER_DAY 

55MICROSECONDS_PER_MONTH = DAYS_PER_MONTH * MICROSECONDS_PER_DAY 

56MICROSECONDS_PER_YEAR = DAYS_PER_YEAR * MICROSECONDS_PER_DAY 

57 

58TIMEDELTA_MAX_DAYS = datetime.timedelta.max.days 

59 

60FractionalComponent = namedtuple( 

61 "FractionalComponent", ["principal", "microsecondremainder"] 

62) 

63 

64 

65def year_range_check(valuestr, limit): 

66 YYYYstr = valuestr 

67 

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

69 # we simply parse to 1900 

70 if len(valuestr) < 4: 

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

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

73 

74 return range_check(YYYYstr, limit) 

75 

76 

77def fractional_range_check(conversion, valuestr, limit): 

78 if valuestr is None: 

79 return None 

80 

81 if "." in valuestr: 

82 castfunc = partial(_cast_to_fractional_component, conversion) 

83 else: 

84 castfunc = int 

85 

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

87 

88 if type(value) is FractionalComponent: 

89 tocheck = float(valuestr) 

90 else: 

91 tocheck = int(valuestr) 

92 

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

94 raise limit.rangeexception(limit.rangeerrorstring) 

95 

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

97 raise limit.rangeexception(limit.rangeerrorstring) 

98 

99 return value 

100 

101 

102def _cast_to_fractional_component(conversion, floatstr): 

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

104 # int representing the floating point remainder as a number 

105 # of microseconds, determined by multiplying by conversion 

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

107 

108 intvalue = int(intpart) 

109 preconvertedvalue = int(floatpart) 

110 

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

112 

113 return FractionalComponent(intvalue, convertedvalue) 

114 

115 

116class PythonTimeBuilder(BaseTimeBuilder): 

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

118 DATE_YYYY_LIMIT = Limit( 

119 "Invalid year string.", 

120 datetime.MINYEAR, 

121 datetime.MAXYEAR, 

122 YearOutOfBoundsError, 

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

124 year_range_check, 

125 ) 

126 TIME_HH_LIMIT = Limit( 

127 "Invalid hour string.", 

128 0, 

129 24, 

130 HoursOutOfBoundsError, 

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

132 partial(fractional_range_check, MICROSECONDS_PER_HOUR), 

133 ) 

134 TIME_MM_LIMIT = Limit( 

135 "Invalid minute string.", 

136 0, 

137 59, 

138 MinutesOutOfBoundsError, 

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

140 partial(fractional_range_check, MICROSECONDS_PER_MINUTE), 

141 ) 

142 TIME_SS_LIMIT = Limit( 

143 "Invalid second string.", 

144 0, 

145 60, 

146 SecondsOutOfBoundsError, 

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

148 partial(fractional_range_check, MICROSECONDS_PER_SECOND), 

149 ) 

150 DURATION_PNY_LIMIT = Limit( 

151 "Invalid year duration string.", 

152 None, 

153 None, 

154 YearOutOfBoundsError, 

155 None, 

156 partial(fractional_range_check, MICROSECONDS_PER_YEAR), 

157 ) 

158 DURATION_PNM_LIMIT = Limit( 

159 "Invalid month duration string.", 

160 None, 

161 None, 

162 MonthOutOfBoundsError, 

163 None, 

164 partial(fractional_range_check, MICROSECONDS_PER_MONTH), 

165 ) 

166 DURATION_PNW_LIMIT = Limit( 

167 "Invalid week duration string.", 

168 None, 

169 None, 

170 WeekOutOfBoundsError, 

171 None, 

172 partial(fractional_range_check, MICROSECONDS_PER_WEEK), 

173 ) 

174 DURATION_PND_LIMIT = Limit( 

175 "Invalid day duration string.", 

176 None, 

177 None, 

178 DayOutOfBoundsError, 

179 None, 

180 partial(fractional_range_check, MICROSECONDS_PER_DAY), 

181 ) 

182 DURATION_TNH_LIMIT = Limit( 

183 "Invalid hour duration string.", 

184 None, 

185 None, 

186 HoursOutOfBoundsError, 

187 None, 

188 partial(fractional_range_check, MICROSECONDS_PER_HOUR), 

189 ) 

190 DURATION_TNM_LIMIT = Limit( 

191 "Invalid minute duration string.", 

192 None, 

193 None, 

194 MinutesOutOfBoundsError, 

195 None, 

196 partial(fractional_range_check, MICROSECONDS_PER_MINUTE), 

197 ) 

198 DURATION_TNS_LIMIT = Limit( 

199 "Invalid second duration string.", 

200 None, 

201 None, 

202 SecondsOutOfBoundsError, 

203 None, 

204 partial(fractional_range_check, MICROSECONDS_PER_SECOND), 

205 ) 

206 

207 DATE_RANGE_DICT = BaseTimeBuilder.DATE_RANGE_DICT 

208 DATE_RANGE_DICT["YYYY"] = DATE_YYYY_LIMIT 

209 

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

211 

212 DURATION_RANGE_DICT = { 

213 "PnY": DURATION_PNY_LIMIT, 

214 "PnM": DURATION_PNM_LIMIT, 

215 "PnW": DURATION_PNW_LIMIT, 

216 "PnD": DURATION_PND_LIMIT, 

217 "TnH": DURATION_TNH_LIMIT, 

218 "TnM": DURATION_TNM_LIMIT, 

219 "TnS": DURATION_TNS_LIMIT, 

220 } 

221 

222 @classmethod 

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

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

225 

226 if MM is None: 

227 MM = 1 

228 

229 if DD is None: 

230 DD = 1 

231 

232 if DDD is not None: 

233 return PythonTimeBuilder._build_ordinal_date(YYYY, DDD) 

234 

235 if Www is not None: 

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

237 

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

239 

240 @classmethod 

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

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

243 # where necessary 

244 hours = 0 

245 minutes = 0 

246 seconds = 0 

247 microseconds = 0 

248 

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

250 

251 if type(hh) is FractionalComponent: 

252 hours = hh.principal 

253 microseconds = hh.microsecondremainder 

254 elif hh is not None: 

255 hours = hh 

256 

257 if type(mm) is FractionalComponent: 

258 minutes = mm.principal 

259 microseconds = mm.microsecondremainder 

260 elif mm is not None: 

261 minutes = mm 

262 

263 if type(ss) is FractionalComponent: 

264 seconds = ss.principal 

265 microseconds = ss.microsecondremainder 

266 elif ss is not None: 

267 seconds = ss 

268 

269 ( 

270 hours, 

271 minutes, 

272 seconds, 

273 microseconds, 

274 ) = PythonTimeBuilder._distribute_microseconds( 

275 microseconds, 

276 (hours, minutes, seconds), 

277 (MICROSECONDS_PER_HOUR, MICROSECONDS_PER_MINUTE, MICROSECONDS_PER_SECOND), 

278 ) 

279 

280 # Move midnight into range 

281 if hours == 24: 

282 hours = 0 

283 

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

285 if tz is not None: 

286 return ( 

287 datetime.datetime( 

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

289 ) 

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

291 ).timetz() 

292 

293 return ( 

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

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

296 ).time() 

297 

298 @classmethod 

299 def build_datetime(cls, date, time): 

300 return datetime.datetime.combine( 

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

302 ) 

303 

304 @classmethod 

305 def build_duration( 

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

307 ): 

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

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

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

311 ) 

312 

313 seconds = TnS.principal 

314 microseconds = TnS.microsecondremainder 

315 

316 return datetime.timedelta( 

317 days=PnD, 

318 seconds=seconds, 

319 microseconds=microseconds, 

320 minutes=TnM, 

321 hours=TnH, 

322 weeks=PnW, 

323 ) 

324 

325 @classmethod 

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

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

328 

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

330 # <start>/<end> 

331 startobject = cls._build_object(start) 

332 endobject = cls._build_object(end) 

333 

334 return (startobject, endobject) 

335 

336 durationobject = cls._build_object(duration) 

337 

338 # Determine if datetime promotion is required 

339 datetimerequired = ( 

340 duration.TnH is not None 

341 or duration.TnM is not None 

342 or duration.TnS is not None 

343 or durationobject.seconds != 0 

344 or durationobject.microseconds != 0 

345 ) 

346 

347 if end is not None: 

348 # <duration>/<end> 

349 endobject = cls._build_object(end) 

350 

351 # Range check 

352 if type(end) is DateTuple and datetimerequired is True: 

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

354 return ( 

355 endobject, 

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

357 ) 

358 

359 return (endobject, endobject - durationobject) 

360 

361 # <start>/<duration> 

362 startobject = cls._build_object(start) 

363 

364 # Range check 

365 if type(start) is DateTuple and datetimerequired is True: 

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

367 return ( 

368 startobject, 

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

370 ) 

371 

372 return (startobject, startobject + durationobject) 

373 

374 @classmethod 

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

376 startobject = None 

377 endobject = None 

378 

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

380 

381 if interval.start is not None: 

382 startobject = cls._build_object(interval.start) 

383 

384 if interval.end is not None: 

385 endobject = cls._build_object(interval.end) 

386 

387 if interval.duration is not None: 

388 durationobject = cls._build_object(interval.duration) 

389 else: 

390 durationobject = endobject - startobject 

391 

392 if R is True: 

393 if startobject is not None: 

394 return cls._date_generator_unbounded(startobject, durationobject) 

395 

396 return cls._date_generator_unbounded(endobject, -durationobject) 

397 

398 iterations = int(Rnn) 

399 

400 if startobject is not None: 

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

402 

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

404 

405 @classmethod 

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

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

408 

409 if Z is True: 

410 # Z -> UTC 

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

412 

413 tzhour = int(hh) 

414 

415 if mm is not None: 

416 tzminute = int(mm) 

417 else: 

418 tzminute = 0 

419 

420 if negative is True: 

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

422 

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

424 

425 @classmethod 

426 def range_check_duration( 

427 cls, 

428 PnY=None, 

429 PnM=None, 

430 PnW=None, 

431 PnD=None, 

432 TnH=None, 

433 TnM=None, 

434 TnS=None, 

435 rangedict=None, 

436 ): 

437 years = 0 

438 months = 0 

439 days = 0 

440 weeks = 0 

441 hours = 0 

442 minutes = 0 

443 seconds = 0 

444 microseconds = 0 

445 

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

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

448 ) 

449 

450 if PnY is not None: 

451 if type(PnY) is FractionalComponent: 

452 years = PnY.principal 

453 microseconds = PnY.microsecondremainder 

454 else: 

455 years = PnY 

456 

457 if years * DAYS_PER_YEAR > TIMEDELTA_MAX_DAYS: 

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

459 

460 if PnM is not None: 

461 if type(PnM) is FractionalComponent: 

462 months = PnM.principal 

463 microseconds = PnM.microsecondremainder 

464 else: 

465 months = PnM 

466 

467 if months * DAYS_PER_MONTH > TIMEDELTA_MAX_DAYS: 

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

469 

470 if PnW is not None: 

471 if type(PnW) is FractionalComponent: 

472 weeks = PnW.principal 

473 microseconds = PnW.microsecondremainder 

474 else: 

475 weeks = PnW 

476 

477 if weeks * DAYS_PER_WEEK > TIMEDELTA_MAX_DAYS: 

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

479 

480 if PnD is not None: 

481 if type(PnD) is FractionalComponent: 

482 days = PnD.principal 

483 microseconds = PnD.microsecondremainder 

484 else: 

485 days = PnD 

486 

487 if days > TIMEDELTA_MAX_DAYS: 

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

489 

490 if TnH is not None: 

491 if type(TnH) is FractionalComponent: 

492 hours = TnH.principal 

493 microseconds = TnH.microsecondremainder 

494 else: 

495 hours = TnH 

496 

497 if hours // HOURS_PER_DAY > TIMEDELTA_MAX_DAYS: 

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

499 

500 if TnM is not None: 

501 if type(TnM) is FractionalComponent: 

502 minutes = TnM.principal 

503 microseconds = TnM.microsecondremainder 

504 else: 

505 minutes = TnM 

506 

507 if minutes // MINUTES_PER_DAY > TIMEDELTA_MAX_DAYS: 

508 raise MinutesOutOfBoundsError( 

509 "Duration exceeds maximum timedelta size." 

510 ) 

511 

512 if TnS is not None: 

513 if type(TnS) is FractionalComponent: 

514 seconds = TnS.principal 

515 microseconds = TnS.microsecondremainder 

516 else: 

517 seconds = TnS 

518 

519 if seconds // SECONDS_PER_DAY > TIMEDELTA_MAX_DAYS: 

520 raise SecondsOutOfBoundsError( 

521 "Duration exceeds maximum timedelta size." 

522 ) 

523 

524 ( 

525 years, 

526 months, 

527 weeks, 

528 days, 

529 hours, 

530 minutes, 

531 seconds, 

532 microseconds, 

533 ) = PythonTimeBuilder._distribute_microseconds( 

534 microseconds, 

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

536 ( 

537 MICROSECONDS_PER_YEAR, 

538 MICROSECONDS_PER_MONTH, 

539 MICROSECONDS_PER_WEEK, 

540 MICROSECONDS_PER_DAY, 

541 MICROSECONDS_PER_HOUR, 

542 MICROSECONDS_PER_MINUTE, 

543 MICROSECONDS_PER_SECOND, 

544 ), 

545 ) 

546 

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

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

549 

550 # Check against timedelta limits 

551 if ( 

552 totaldays 

553 + weeks * DAYS_PER_WEEK 

554 + hours // HOURS_PER_DAY 

555 + minutes // MINUTES_PER_DAY 

556 + seconds // SECONDS_PER_DAY 

557 > TIMEDELTA_MAX_DAYS 

558 ): 

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

560 

561 return ( 

562 None, 

563 None, 

564 weeks, 

565 totaldays, 

566 hours, 

567 minutes, 

568 FractionalComponent(seconds, microseconds), 

569 ) 

570 

571 @classmethod 

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

573 # Handles concise format, range checks any potential durations 

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

575 # <start>/<end> 

576 # Handle concise format 

577 if cls._is_interval_end_concise(end) is True: 

578 end = cls._combine_concise_interval_tuples(start, end) 

579 

580 return (start, end, duration) 

581 

582 durationobject = cls._build_object(duration) 

583 

584 if end is not None: 

585 # <duration>/<end> 

586 endobject = cls._build_object(end) 

587 

588 # Range check 

589 if type(end) is DateTuple: 

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

591 

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

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

594 else: 

595 mindatetime = datetime.datetime.min 

596 

597 if end.time.tz is not None: 

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

599 

600 if endobject - mindatetime < durationobject: 

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

602 else: 

603 # <start>/<duration> 

604 startobject = cls._build_object(start) 

605 

606 # Range check 

607 if type(start) is DateTuple: 

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

609 

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

611 raise YearOutOfBoundsError( 

612 "Interval end greater than maximum date." 

613 ) 

614 else: 

615 maxdatetime = datetime.datetime.max 

616 

617 if start.time.tz is not None: 

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

619 

620 if maxdatetime - startobject < durationobject: 

621 raise YearOutOfBoundsError( 

622 "Interval end greater than maximum date." 

623 ) 

624 

625 return (start, end, duration) 

626 

627 @staticmethod 

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

629 if isoday is None: 

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

631 weeks=isoweek - 1 

632 ) 

633 

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

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

636 ) 

637 

638 @staticmethod 

639 def _build_ordinal_date(isoyear, isoday): 

640 # Day of year to a date 

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

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

643 

644 return builtdate 

645 

646 @staticmethod 

647 def _iso_year_start(isoyear): 

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

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

650 # Stolen from: 

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

652 

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

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

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

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

657 

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

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

660 

661 # Return the start of the year 

662 return fourth_jan - delta 

663 

664 @staticmethod 

665 def _date_generator(startdate, timedelta, iterations): 

666 currentdate = startdate 

667 currentiteration = 0 

668 

669 while currentiteration < iterations: 

670 yield currentdate 

671 

672 # Update the values 

673 currentdate += timedelta 

674 currentiteration += 1 

675 

676 @staticmethod 

677 def _date_generator_unbounded(startdate, timedelta): 

678 currentdate = startdate 

679 

680 while True: 

681 yield currentdate 

682 

683 # Update the value 

684 currentdate += timedelta 

685 

686 @staticmethod 

687 def _distribute_microseconds(todistribute, recipients, reductions): 

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

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

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

691 # todistribute divided across recipients using the reductions, with 

692 # the final remainder returned as the final tuple member 

693 results = [] 

694 

695 remainder = todistribute 

696 

697 for index, reduction in enumerate(reductions): 

698 additional, remainder = divmod(remainder, reduction) 

699 

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

701 

702 # Always return the remaining microseconds 

703 results.append(remainder) 

704 

705 return tuple(results)