1use crate::internal_prelude::*;
2use crate::time::constants::*;
3use crate::time::Instant;
4use sbor::rust::fmt;
5use sbor::rust::fmt::Display;
6use sbor::rust::num::ParseIntError;
7use sbor::rust::str::FromStr;
8use sbor::rust::vec::Vec;
9
10const UNIX_EPOCH_YEAR: u32 = 1970;
11
12const SECONDS_IN_A_NON_LEAP_YEAR: i64 = 365 * 24 * 60 * 60;
13const SECONDS_IN_A_LEAP_YEAR: i64 = 366 * 24 * 60 * 60;
14
15const DAYS_PER_4Y: i64 = 365 * 4 + 1;
16const DAYS_PER_100Y: i64 = 365 * 100 + 24;
17const DAYS_PER_400Y: i64 = 365 * 400 + 97;
18
19const LEAP_YEAR_DAYS_IN_MONTHS: [u8; 12] = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
20
21const SHIFT_FROM_UNIX_TIME_TO_MARCH_Y2K: i64 = 946684800 + 86400 * (31 + 29);
32
33const MIN_SUPPORTED_TIMESTAMP: i64 = -62135596800;
37
38const MAX_SUPPORTED_TIMESTAMP: i64 = 135536014634284799;
43
44#[derive(Sbor, PartialEq, Eq, Copy, Clone, Debug)]
45pub enum DateTimeError {
46 InvalidYear,
47 InvalidMonth,
48 InvalidDayOfMonth,
49 InvalidHour,
50 InvalidMinute,
51 InvalidSecond,
52 InstantIsOutOfRange,
53}
54
55#[cfg(not(feature = "alloc"))]
56impl std::error::Error for DateTimeError {}
57
58impl fmt::Display for DateTimeError {
59 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
60 match self {
61 DateTimeError::InvalidYear =>
62 write!(f, "Invalid year. Expected a value strictly greater than 0"),
63 DateTimeError::InvalidMonth =>
64 write!(f, "Invalid month. Expected a value between 1 (inclusive) and 12 (inclusive)"),
65 DateTimeError::InvalidDayOfMonth =>
66 write!(f, "Invalid day of month. Expected a value between 1 (inclusive) and, depending on a month, 28, 29 (Feb on a leap year), 30 or 31 (inclusive)"),
67 DateTimeError::InvalidHour =>
68 write!(f, "Invalid hour. Expected a value between 0 (inclusive) and 23 (inclusive)"),
69 DateTimeError::InvalidMinute =>
70 write!(f, "Invalid minute. Expected a value between 0 (inclusive) and 59 (inclusive)"),
71 DateTimeError::InvalidSecond =>
72 write!(f, "Invalid second. Expected a value between 0 (inclusive) and 59 (inclusive)"),
73 DateTimeError::InstantIsOutOfRange =>
74 write!(f, "Instant out of supported range [{}, {}]", MIN_SUPPORTED_TIMESTAMP, MAX_SUPPORTED_TIMESTAMP),
75 }
76 }
77}
78
79#[derive(
92 PartialEq,
93 Eq,
94 Copy,
95 Clone,
96 Debug,
97 Categorize,
98 Encode,
99 Decode,
100 BasicDescribe,
101 PartialOrd,
102 Ord,
103 Hash,
104)]
105pub struct UtcDateTime {
106 year: u32,
107 month: u8,
108 day_of_month: u8,
109 hour: u8,
110 minute: u8,
111 second: u8,
112}
113
114impl Describe<ScryptoCustomTypeKind> for UtcDateTime {
115 const TYPE_ID: RustTypeId =
116 RustTypeId::WellKnown(well_known_scrypto_custom_types::UTC_DATE_TIME_TYPE);
117
118 fn type_data() -> TypeData<ScryptoCustomTypeKind, RustTypeId> {
119 well_known_scrypto_custom_types::utc_date_time_type_data()
120 }
121}
122
123impl UtcDateTime {
124 pub fn new(
139 year: u32,
140 month: u8,
141 day_of_month: u8,
142 hour: u8,
143 minute: u8,
144 second: u8,
145 ) -> Result<Self, DateTimeError> {
146 if year == 0 {
147 return Err(DateTimeError::InvalidYear);
148 }
149
150 if !(1..=12).contains(&month) {
151 return Err(DateTimeError::InvalidMonth);
152 }
153
154 if day_of_month < 1 ||
155 day_of_month > LEAP_YEAR_DAYS_IN_MONTHS[(month - 1) as usize] ||
157 (!Self::is_leap_year(year) && month == 2 && day_of_month > 28)
159 {
160 return Err(DateTimeError::InvalidDayOfMonth);
161 }
162
163 if hour > 23 {
164 return Err(DateTimeError::InvalidHour);
165 }
166
167 if minute > 59 {
168 return Err(DateTimeError::InvalidMinute);
169 }
170
171 if second > 59 {
172 return Err(DateTimeError::InvalidSecond);
173 }
174
175 Ok(Self {
176 year,
177 month,
178 day_of_month,
179 hour,
180 minute,
181 second,
182 })
183 }
184
185 pub fn from_instant(instant: &Instant) -> Result<Self, DateTimeError> {
190 if instant.seconds_since_unix_epoch < MIN_SUPPORTED_TIMESTAMP
191 || instant.seconds_since_unix_epoch > MAX_SUPPORTED_TIMESTAMP
192 {
193 return Err(DateTimeError::InstantIsOutOfRange);
194 }
195
196 let secs_since_march_y2k =
198 instant.seconds_since_unix_epoch - SHIFT_FROM_UNIX_TIME_TO_MARCH_Y2K;
199
200 let mut days_since_march_y2k = secs_since_march_y2k / SECONDS_IN_A_DAY;
201 let mut remaining_secs = secs_since_march_y2k % SECONDS_IN_A_DAY;
202 if remaining_secs < 0 {
203 remaining_secs += SECONDS_IN_A_DAY;
204 days_since_march_y2k -= 1;
205 }
206
207 let mut num_400_year_cycles = days_since_march_y2k / DAYS_PER_400Y;
208 let mut remaining_days = days_since_march_y2k % DAYS_PER_400Y;
209 if remaining_days < 0 {
210 remaining_days += DAYS_PER_400Y;
211 num_400_year_cycles -= 1;
212 }
213
214 let mut num_100_year_cycles = remaining_days / DAYS_PER_100Y;
215 if num_100_year_cycles == 4 {
216 num_100_year_cycles -= 1;
218 }
219 remaining_days -= num_100_year_cycles * DAYS_PER_100Y;
220
221 let mut num_4_year_cycles = remaining_days / DAYS_PER_4Y;
222 if num_4_year_cycles == 25 {
223 num_4_year_cycles -= 1;
225 }
226 remaining_days -= num_4_year_cycles * DAYS_PER_4Y;
227
228 let mut remaining_years = remaining_days / 365;
229 if remaining_years == 4 {
230 remaining_years -= 1;
232 }
233 remaining_days -= remaining_years * 365;
234
235 let mut year =
236 remaining_years +
237 4 * num_4_year_cycles +
238 100 * num_100_year_cycles +
239 400 * num_400_year_cycles
240 + 2000 ;
241
242 let mut days_in_months_starting_on_march = LEAP_YEAR_DAYS_IN_MONTHS;
243 days_in_months_starting_on_march.rotate_left(2);
244
245 let mut month = 0;
246 while days_in_months_starting_on_march[month] as i64 <= remaining_days {
247 remaining_days -= days_in_months_starting_on_march[month] as i64;
248 month += 1;
249 }
250
251 month += 2;
254 if month >= 12 {
255 month -= 12;
256 year += 1;
257 }
258
259 month += 1;
261
262 let day_of_month = remaining_days + 1;
264
265 let hour = remaining_secs / SECONDS_IN_AN_HOUR;
266 let minute = remaining_secs / SECONDS_IN_A_MINUTE % SECONDS_IN_A_MINUTE;
267 let second = remaining_secs % SECONDS_IN_A_MINUTE;
268
269 Ok(Self {
270 year: u32::try_from(year).expect("year overflow"),
271 month: u8::try_from(month).expect("month overflow"),
272 day_of_month: u8::try_from(day_of_month).expect("day_of_month overflow"),
273 hour: u8::try_from(hour).expect("hour overflow"),
274 minute: u8::try_from(minute).expect("minute overflow"),
275 second: u8::try_from(second).expect("second overflow"),
276 })
277 }
278
279 pub fn to_instant(&self) -> Instant {
281 let is_leap_year = Self::is_leap_year(self.year);
282
283 if self.year >= UNIX_EPOCH_YEAR {
286 let num_leap_years_between_self_and_epoch =
288 (Self::num_leap_years_up_to_exclusive(self.year)
289 - Self::num_leap_years_up_to_exclusive(UNIX_EPOCH_YEAR + 1))
290 as i64;
291
292 let num_non_leap_years_between_self_and_epoch =
293 (self.year - UNIX_EPOCH_YEAR) as i64 - num_leap_years_between_self_and_epoch;
294
295 let seconds_up_to_the_beginning_of_the_year = (num_non_leap_years_between_self_and_epoch
297 * SECONDS_IN_A_NON_LEAP_YEAR)
298 + (num_leap_years_between_self_and_epoch * SECONDS_IN_A_LEAP_YEAR);
299
300 let mut seconds_in_ended_months = 0;
302 for n in 0..self.month - 1 {
303 seconds_in_ended_months +=
304 LEAP_YEAR_DAYS_IN_MONTHS[n as usize] as i64 * SECONDS_IN_A_DAY;
305 if !is_leap_year && n == 1 {
307 seconds_in_ended_months -= SECONDS_IN_A_DAY;
308 }
309 }
310
311 let total_seconds_since_unix_epoch = seconds_up_to_the_beginning_of_the_year
313 + seconds_in_ended_months
314 + (self.day_of_month - 1) as i64 * SECONDS_IN_A_DAY
315 + self.hour as i64 * SECONDS_IN_AN_HOUR
316 + self.minute as i64 * SECONDS_IN_A_MINUTE
317 + self.second as i64;
318
319 Instant::new(total_seconds_since_unix_epoch)
320 } else {
321 let num_leap_years_between_epoch_and_self =
323 (Self::num_leap_years_up_to_exclusive(UNIX_EPOCH_YEAR)
324 - Self::num_leap_years_up_to_exclusive(self.year + 1)) as i64;
325
326 let num_non_leap_days_between_epoch_and_self =
327 (UNIX_EPOCH_YEAR - self.year - 1) as i64 - num_leap_years_between_epoch_and_self;
328
329 let seconds_up_to_the_end_of_the_year = (num_non_leap_days_between_epoch_and_self
332 * SECONDS_IN_A_NON_LEAP_YEAR)
333 + (num_leap_years_between_epoch_and_self * SECONDS_IN_A_LEAP_YEAR);
334
335 let mut seconds_in_non_started_months = 0;
337 let mut curr_month = 11;
338 while curr_month > self.month - 1 {
339 seconds_in_non_started_months +=
340 LEAP_YEAR_DAYS_IN_MONTHS[curr_month as usize] as i64 * SECONDS_IN_A_DAY;
341 if !is_leap_year && curr_month == 1 {
343 seconds_in_non_started_months -= SECONDS_IN_A_DAY;
344 }
345 curr_month -= 1;
346 }
347
348 let mut days_in_month = LEAP_YEAR_DAYS_IN_MONTHS[self.month as usize - 1] as i64;
349 if !is_leap_year && curr_month == 1 {
350 days_in_month -= 1;
351 }
352
353 let remaining_days_in_month = days_in_month - self.day_of_month as i64;
355
356 let total_seconds_since_unix_epoch = seconds_up_to_the_end_of_the_year
357 + seconds_in_non_started_months
358 + remaining_days_in_month * SECONDS_IN_A_DAY
359 + (23 - self.hour) as i64 * SECONDS_IN_AN_HOUR
360 + (59 - self.minute) as i64 * SECONDS_IN_A_MINUTE
361 + (59 - self.second) as i64;
362
363 Instant::new(
364 -total_seconds_since_unix_epoch - 1,
366 )
367 }
368 }
369
370 fn num_leap_years_up_to_exclusive(year: u32) -> u32 {
371 let prev = year - 1;
372 (prev / 4) - (prev / 100) + (prev / 400)
373 }
374
375 fn is_leap_year(year: u32) -> bool {
376 year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
377 }
378
379 pub fn year(&self) -> u32 {
380 self.year
381 }
382
383 pub fn month(&self) -> u8 {
384 self.month
385 }
386
387 pub fn day_of_month(&self) -> u8 {
388 self.day_of_month
389 }
390
391 pub fn hour(&self) -> u8 {
392 self.hour
393 }
394
395 pub fn minute(&self) -> u8 {
396 self.minute
397 }
398
399 pub fn second(&self) -> u8 {
400 self.second
401 }
402
403 pub fn add_days(&self, days_to_add: i64) -> Option<UtcDateTime> {
404 self.to_instant()
405 .add_days(days_to_add)
406 .and_then(|i| Self::from_instant(&i).ok())
407 }
408
409 pub fn add_hours(&self, hours_to_add: i64) -> Option<UtcDateTime> {
410 self.to_instant()
411 .add_hours(hours_to_add)
412 .and_then(|i| Self::from_instant(&i).ok())
413 }
414
415 pub fn add_minutes(&self, minutes_to_add: i64) -> Option<UtcDateTime> {
416 self.to_instant()
417 .add_minutes(minutes_to_add)
418 .and_then(|i| Self::from_instant(&i).ok())
419 }
420
421 pub fn add_seconds(&self, seconds_to_add: i64) -> Option<UtcDateTime> {
422 self.to_instant()
423 .add_seconds(seconds_to_add)
424 .and_then(|i| Self::from_instant(&i).ok())
425 }
426}
427
428impl TryFrom<Instant> for UtcDateTime {
429 type Error = DateTimeError;
430 fn try_from(instant: Instant) -> Result<Self, Self::Error> {
431 UtcDateTime::from_instant(&instant)
432 }
433}
434
435impl From<UtcDateTime> for Instant {
436 fn from(dt: UtcDateTime) -> Self {
437 dt.to_instant()
438 }
439}
440
441impl Display for UtcDateTime {
442 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
443 write!(
444 f,
445 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
446 self.year, self.month, self.day_of_month, self.hour, self.minute, self.second,
447 )
448 }
449}
450
451#[derive(Debug, Clone)]
452pub enum ParseUtcDateTimeError {
453 InvalidFormat,
454 DateTimeError(DateTimeError),
455}
456
457#[cfg(not(feature = "alloc"))]
458impl std::error::Error for ParseUtcDateTimeError {}
459
460impl fmt::Display for ParseUtcDateTimeError {
461 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
462 match self {
463 ParseUtcDateTimeError::InvalidFormat => write!(f, "Invalid date time format. Must be in ISO-8601 format, up to second precision, such as '2011-12-03T10:15:30Z'."),
464 ParseUtcDateTimeError::DateTimeError(e) => fmt::Display::fmt(e, f)
465 }
466 }
467}
468
469impl From<ParseIntError> for ParseUtcDateTimeError {
470 fn from(_value: ParseIntError) -> Self {
471 Self::InvalidFormat
472 }
473}
474
475impl From<DateTimeError> for ParseUtcDateTimeError {
476 fn from(value: DateTimeError) -> Self {
477 Self::DateTimeError(value)
478 }
479}
480
481impl FromStr for UtcDateTime {
482 type Err = ParseUtcDateTimeError;
483
484 fn from_str(s: &str) -> Result<Self, Self::Err> {
485 let chars: Vec<char> = s.chars().collect();
486 if chars.len() == 20
487 && chars[4] == '-'
488 && chars[7] == '-'
489 && chars[10] == 'T'
490 && chars[13] == ':'
491 && chars[16] == ':'
492 && chars[19] == 'Z'
493 {
494 Ok(UtcDateTime::new(
495 s[0..4].parse::<u32>()?,
496 s[5..7].parse::<u8>()?,
497 s[8..10].parse::<u8>()?,
498 s[11..13].parse::<u8>()?,
499 s[14..16].parse::<u8>()?,
500 s[17..19].parse::<u8>()?,
501 )?)
502 } else {
503 Err(ParseUtcDateTimeError::InvalidFormat)
504 }
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511 use crate::time::utc_date_time::MAX_SUPPORTED_TIMESTAMP;
512 use radix_common::time::utc_date_time::MIN_SUPPORTED_TIMESTAMP;
513
514 #[test]
515 pub fn test_to_string() {
516 let expected_str = "2023-01-27T12:17:25Z";
517 let instant = Instant {
518 seconds_since_unix_epoch: 1674821845,
519 };
520 let date_time = UtcDateTime::from_instant(&instant).unwrap();
521 assert_eq!(date_time.to_string(), expected_str);
522 assert_eq!(format!("{}", date_time), expected_str);
523 assert_eq!(
524 UtcDateTime::from_str(expected_str).unwrap().to_instant(),
525 instant
526 );
527 }
528
529 #[test]
530 pub fn test_instant_date_time_conversions() {
531 let test_data = vec![
532 (MIN_SUPPORTED_TIMESTAMP, [1, 1, 1, 0, 0, 0]),
533 (-62104060800, [2, 1, 1, 0, 0, 0]),
534 (-62035804801, [4, 2, 29, 23, 59, 59]),
535 (-30578688000, [1001, 1, 1, 0, 0, 0]),
536 (-5233420801, [1804, 2, 28, 23, 59, 59]),
537 (-2147483648, [1901, 12, 13, 20, 45, 52]),
538 (-58147200, [1968, 2, 28, 00, 00, 00]),
539 (-58147199, [1968, 2, 28, 00, 00, 1]),
540 (-58060801, [1968, 2, 28, 23, 59, 59]),
541 (-58060800, [1968, 2, 29, 00, 00, 00]),
542 (-1, [1969, 12, 31, 23, 59, 59]),
543 (0, [1970, 1, 1, 0, 0, 0]),
544 (1, [1970, 1, 1, 0, 0, 1]),
545 (365 * 24 * 60 * 60, [1971, 1, 1, 0, 0, 0]),
546 (366 * 24 * 60 * 60, [1971, 1, 2, 0, 0, 0]),
547 (395 * 24 * 60 * 60, [1971, 1, 31, 0, 0, 0]),
548 (396 * 24 * 60 * 60, [1971, 2, 1, 0, 0, 0]),
549 (68180521, [1972, 2, 29, 3, 2, 1]),
550 (194476271, [1976, 2, 29, 21, 11, 11]),
551 (446947199, [1984, 2, 29, 23, 59, 59]),
552 (447012859, [1984, 3, 1, 18, 14, 19]),
553 (951865200, [2000, 2, 29, 23, 0, 0]),
554 (951868800, [2000, 3, 1, 0, 0, 0]),
555 (1109548800, [2005, 2, 28, 0, 0, 0]),
556 (1670420819, [2022, 12, 7, 13, 46, 59]),
557 (1835395199, [2028, 2, 28, 23, 59, 59]),
558 (1835395200, [2028, 2, 29, 00, 00, 00]),
559 (1835481599, [2028, 2, 29, 23, 59, 59]),
560 (1835481600, [2028, 3, 1, 00, 00, 00]),
561 (51442991999, [3600, 2, 29, 23, 59, 59]),
562 (64065686400, [4000, 2, 29, 0, 0, 0]),
563 (569034205384, [20001, 12, 23, 1, 3, 4]),
564 (MAX_SUPPORTED_TIMESTAMP, [u32::MAX, 12, 31, 23, 59, 59]),
565 ];
566
567 for (timestamp, dt_components) in test_data {
568 let expected_dt = UtcDateTime::from(dt_components);
569 let expected_instant = Instant::new(timestamp);
570
571 assert_eq!(expected_dt.to_instant(), expected_instant);
572 assert_eq!(
573 UtcDateTime::from_instant(&expected_instant).unwrap(),
574 expected_dt
575 );
576 }
577
578 assert!(UtcDateTime::from_instant(&Instant::new(MAX_SUPPORTED_TIMESTAMP + 1)).is_err());
580 assert!(UtcDateTime::from_instant(&Instant::new(MIN_SUPPORTED_TIMESTAMP - 1)).is_err());
581 assert!(UtcDateTime::from_instant(&Instant::new(i64::MIN)).is_err());
582 assert!(UtcDateTime::from_instant(&Instant::new(i64::MAX)).is_err());
583 }
584
585 #[test]
586 pub fn test_date_time_add_xyz_methods() {
587 assert_dates(
588 [2022, 1, 1, 12, 12, 12],
589 |dt| dt.add_days(2),
590 [2022, 1, 3, 12, 12, 12],
591 );
592
593 assert_dates(
594 [1968, 2, 29, 00, 00, 00],
595 |dt| dt.add_days(2).and_then(|dt| dt.add_hours(2)),
596 [1968, 3, 2, 2, 00, 00],
597 );
598
599 assert_dates(
600 [2028, 2, 29, 23, 59, 59],
601 |dt| dt.add_hours(49).and_then(|dt| dt.add_seconds(1)),
602 [2028, 3, 3, 1, 00, 00],
603 );
604
605 assert_dates(
606 [2022, 1, 1, 12, 12, 12],
607 |dt| dt.add_days(2),
608 [2022, 1, 3, 12, 12, 12],
609 );
610
611 assert_dates(
612 [1, 1, 1, 0, 0, 0],
613 |dt| dt.add_minutes(1000 * 365 * 23 * 60),
614 [958, 9, 12, 16, 0, 0],
615 );
616
617 assert_dates(
618 [1970, 1, 1, 0, 0, 0],
619 |dt| dt.add_days(365),
620 [1971, 1, 1, 0, 0, 0],
621 );
622
623 assert_dates(
624 [1971, 1, 1, 0, 0, 0],
625 |dt| dt.add_days(-365),
626 [1970, 1, 1, 0, 0, 0],
627 );
628
629 assert_dates(
630 [1968, 3, 1, 00, 00, 00],
631 |dt| dt.add_seconds(-1),
632 [1968, 2, 29, 23, 59, 59],
633 );
634
635 assert_fails([u32::MAX, 12, 31, 00, 00, 00], |dt| dt.add_days(1));
636 assert_fails([u32::MAX, 12, 31, 23, 59, 59], |dt| dt.add_hours(1));
637 assert_fails([u32::MAX, 12, 31, 23, 59, 59], |dt| dt.add_minutes(1));
638 assert_fails([u32::MAX, 12, 31, 23, 59, 59], |dt| dt.add_seconds(1));
639
640 assert_fails([2000, 12, 31, 23, 59, 59], |dt| dt.add_days(i64::MAX));
641 assert_fails([2000, 12, 31, 23, 59, 59], |dt| dt.add_hours(i64::MAX));
642 assert_fails([2000, 12, 31, 23, 59, 59], |dt| dt.add_minutes(i64::MAX));
643 assert_fails([2000, 12, 31, 23, 59, 59], |dt| dt.add_seconds(i64::MAX));
644
645 assert_fails([2000, 12, 31, 23, 59, 59], |dt| dt.add_days(i64::MIN));
646 assert_fails([2000, 12, 31, 23, 59, 59], |dt| dt.add_hours(i64::MIN));
647 assert_fails([2000, 12, 31, 23, 59, 59], |dt| dt.add_minutes(i64::MIN));
648 assert_fails([2000, 12, 31, 23, 59, 59], |dt| dt.add_seconds(i64::MIN));
649 }
650
651 fn assert_dates<F: FnOnce(UtcDateTime) -> Option<UtcDateTime>>(
652 start: [u32; 6],
653 op: F,
654 expected_end: [u32; 6],
655 ) {
656 let start_dt = UtcDateTime::from(start);
657 let expected_end_dt = UtcDateTime::from(expected_end);
658 assert_eq!(op(start_dt).unwrap(), expected_end_dt);
659 }
660
661 fn assert_fails<F: FnOnce(UtcDateTime) -> Option<UtcDateTime>>(start: [u32; 6], op: F) {
662 let start_dt = UtcDateTime::from(start);
663 assert!(op(start_dt).is_none());
664 }
665
666 impl From<[u32; 6]> for UtcDateTime {
667 fn from(dt: [u32; 6]) -> UtcDateTime {
668 UtcDateTime::new(
669 dt[0],
670 dt[1] as u8,
671 dt[2] as u8,
672 dt[3] as u8,
673 dt[4] as u8,
674 dt[5] as u8,
675 )
676 .unwrap()
677 }
678 }
679}