1 /* $OpenBSD: day.c,v 1.37 2019/08/12 20:03:28 millert Exp $ */ 2 3 /* 4 * Copyright (c) 1989, 1993, 1994 5 * The Regents of the University of California. All rights reserved. 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 1. Redistributions of source code must retain the above copyright 11 * notice, this list of conditions and the following disclaimer. 12 * 2. Redistributions in binary form must reproduce the above copyright 13 * notice, this list of conditions and the following disclaimer in the 14 * documentation and/or other materials provided with the distribution. 15 * 3. Neither the name of the University nor the names of its contributors 16 * may be used to endorse or promote products derived from this software 17 * without specific prior written permission. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 20 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 23 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 25 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 26 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 28 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 29 * SUCH DAMAGE. 30 */ 31 32 #include <sys/types.h> 33 #include <sys/uio.h> 34 35 #include <ctype.h> 36 #include <err.h> 37 #include <locale.h> 38 #include <stdio.h> 39 #include <stdlib.h> 40 #include <string.h> 41 #include <time.h> 42 43 #include "pathnames.h" 44 #include "calendar.h" 45 46 extern struct iovec header[]; 47 48 #define WEEKLY 1 49 #define MONTHLY 2 50 #define YEARLY 3 51 52 struct tm *tp; 53 int *cumdays, offset; 54 char dayname[10]; 55 enum calendars calendar; 56 u_long julian; 57 58 59 /* 1-based month, 0-based days, cumulative */ 60 int daytab[][14] = { 61 { 0, -1, 30, 58, 89, 119, 150, 180, 211, 242, 272, 303, 333, 364 }, 62 { 0, -1, 30, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 }, 63 }; 64 65 static char *days[] = { 66 "sun", "mon", "tue", "wed", "thu", "fri", "sat", NULL, 67 }; 68 69 static char *months[] = { 70 "jan", "feb", "mar", "apr", "may", "jun", 71 "jul", "aug", "sep", "oct", "nov", "dec", NULL, 72 }; 73 74 static struct fixs fndays[8]; /* full national days names */ 75 static struct fixs ndays[8]; /* short national days names */ 76 77 static struct fixs fnmonths[13]; /* full national months names */ 78 static struct fixs nmonths[13]; /* short national month names */ 79 80 void 81 fill_print_date(struct match *m, struct tm *tm) 82 { 83 if (strftime(m->print_date, sizeof(m->print_date), 84 daynames ? "%a %b %d" : "%b %d", tm) == 0) 85 m->print_date[sizeof(m->print_date) - 1] = '\0'; 86 } 87 88 void 89 setnnames(void) 90 { 91 char buf[80]; 92 int i, l; 93 struct tm tm; 94 95 for (i = 0; i < 7; i++) { 96 tm.tm_wday = i; 97 l = strftime(buf, sizeof(buf), "%a", &tm); 98 for (; l > 0 && isspace((unsigned char)buf[l - 1]); l--) 99 ; 100 buf[l] = '\0'; 101 free(ndays[i].name); 102 if ((ndays[i].name = strdup(buf)) == NULL) 103 err(1, NULL); 104 ndays[i].len = strlen(buf); 105 106 l = strftime(buf, sizeof(buf), "%A", &tm); 107 for (; l > 0 && isspace((unsigned char)buf[l - 1]); l--) 108 ; 109 buf[l] = '\0'; 110 free(fndays[i].name); 111 if ((fndays[i].name = strdup(buf)) == NULL) 112 err(1, NULL); 113 fndays[i].len = strlen(buf); 114 } 115 116 for (i = 0; i < 12; i++) { 117 tm.tm_mon = i; 118 l = strftime(buf, sizeof(buf), "%b", &tm); 119 for (; l > 0 && isspace((unsigned char)buf[l - 1]); l--) 120 ; 121 buf[l] = '\0'; 122 free(nmonths[i].name); 123 if ((nmonths[i].name = strdup(buf)) == NULL) 124 err(1, NULL); 125 nmonths[i].len = strlen(buf); 126 127 l = strftime(buf, sizeof(buf), "%B", &tm); 128 for (; l > 0 && isspace((unsigned char)buf[l - 1]); l--) 129 ; 130 buf[l] = '\0'; 131 free(fnmonths[i].name); 132 if ((fnmonths[i].name = strdup(buf)) == NULL) 133 err(1, NULL); 134 fnmonths[i].len = strlen(buf); 135 } 136 /* Hardwired special events */ 137 spev[0].name = strdup(PESACH); 138 spev[0].nlen = PESACHLEN; 139 spev[0].getev = pesach; 140 spev[1].name = strdup(EASTER); 141 spev[1].nlen = EASTERNAMELEN; 142 spev[1].getev = easter; 143 spev[2].name = strdup(PASKHA); 144 spev[2].nlen = PASKHALEN; 145 spev[2].getev = paskha; 146 for (i = 0; i < NUMEV; i++) { 147 if (spev[i].name == NULL) 148 err(1, NULL); 149 spev[i].uname = NULL; 150 } 151 } 152 153 void 154 settime(time_t *now) 155 { 156 tp = localtime(now); 157 tp->tm_sec = 0; 158 tp->tm_min = 0; 159 /* Avoid getting caught by a timezone shift; set time to noon */ 160 tp->tm_isdst = 0; 161 tp->tm_hour = 12; 162 *now = mktime(tp); 163 if (isleap(tp->tm_year + 1900)) 164 cumdays = daytab[1]; 165 else 166 cumdays = daytab[0]; 167 /* Friday displays Monday's events */ 168 offset = tp->tm_wday == 5 ? 3 : 1; 169 if (f_Setday) 170 offset = 0; /* Except not when range is set explicitly */ 171 header[5].iov_base = dayname; 172 173 (void) setlocale(LC_TIME, "C"); 174 header[5].iov_len = strftime(dayname, sizeof(dayname), "%A", tp); 175 (void) setlocale(LC_TIME, ""); 176 177 setnnames(); 178 } 179 180 /* convert [Year][Month]Day into unix time (since 1970) 181 * Year: two or four digits, Month: two digits, Day: two digits 182 */ 183 time_t 184 Mktime(char *date) 185 { 186 time_t t; 187 int len; 188 struct tm tm; 189 190 (void)time(&t); 191 tp = localtime(&t); 192 193 len = strlen(date); 194 if (len < 2) 195 return((time_t)-1); 196 bzero(&tm, sizeof tm); 197 tm.tm_sec = 0; 198 tm.tm_min = 0; 199 /* Avoid getting caught by a timezone shift; set time to noon */ 200 tm.tm_isdst = 0; 201 tm.tm_hour = 12; 202 tm.tm_wday = 0; 203 tm.tm_mday = tp->tm_mday; 204 tm.tm_mon = tp->tm_mon; 205 tm.tm_year = tp->tm_year; 206 207 /* Day */ 208 tm.tm_mday = atoi(date + len - 2); 209 210 /* Month */ 211 if (len >= 4) { 212 *(date + len - 2) = '\0'; 213 tm.tm_mon = atoi(date + len - 4) - 1; 214 } 215 216 /* Year */ 217 if (len >= 6) { 218 *(date + len - 4) = '\0'; 219 tm.tm_year = atoi(date); 220 221 if (tm.tm_year < 69) /* Y2K */ 222 tm.tm_year += 100; 223 else if (tm.tm_year > 1900) 224 tm.tm_year -= 1900; 225 } 226 227 #if DEBUG 228 printf("Mktime: %d %lld %d %s\n", (int)mktime(&tm), (long long)t, len, 229 asctime(&tm)); 230 #endif 231 return(mktime(&tm)); 232 } 233 234 static void 235 adjust_calendar(int *day, int *month) 236 { 237 switch (calendar) { 238 case GREGORIAN: 239 break; 240 241 case JULIAN: 242 *day += julian; 243 if (*day > (cumdays[*month + 1] - cumdays[*month])) { 244 *day -= (cumdays[*month + 1] - cumdays[*month]); 245 if (++*month > 12) 246 *month = 1; 247 } 248 break; 249 case LUNAR: 250 break; 251 } 252 } 253 254 /* 255 * Possible date formats include any combination of: 256 * 3-charmonth (January, Jan, Jan) 257 * 3-charweekday (Friday, Monday, mon.) 258 * numeric month or day (1, 2, 04) 259 * 260 * Any character except \t or '*' may separate them, or they may not be 261 * separated. Any line following a line that is matched, that starts 262 * with \t, is shown along with the matched line. 263 */ 264 struct match * 265 isnow(char *endp, int bodun) 266 { 267 int day = 0, flags = 0, month = 0, v1, v2, i; 268 int monthp, dayp, varp = 0; 269 struct match *matches = NULL, *tmp, *tmp2; 270 int interval = YEARLY; /* how frequently the event repeats. */ 271 int vwd = 0; /* Variable weekday */ 272 time_t tdiff, ttmp; 273 struct tm tmtmp; 274 275 /* 276 * CONVENTION 277 * 278 * Month: 1-12 279 * Monthname: Jan .. Dec 280 * Day: 1-31 281 * Weekday: Mon-Sun 282 * 283 */ 284 285 /* read first field */ 286 /* didn't recognize anything, skip it */ 287 if (!(v1 = getfield(endp, &endp, &flags))) 288 return (NULL); 289 290 /* adjust bodun rate */ 291 if (bodun && !bodun_always) 292 bodun = !arc4random_uniform(3); 293 294 /* Easter or Easter depending days */ 295 if (flags & F_SPECIAL) 296 vwd = v1; 297 298 /* 299 * 1. {Weekday,Day} XYZ ... 300 * 301 * where Day is > 12 302 */ 303 else if (flags & F_ISDAY || v1 > 12) { 304 305 /* found a day; day: 13-31 or weekday: 1-7 */ 306 day = v1; 307 308 /* {Day,Weekday} {Month,Monthname} ... */ 309 /* if no recognizable month, assume just a day alone -- this is 310 * very unlikely and can only happen after the first 12 days. 311 * --find month or use current month */ 312 if (!(month = getfield(endp, &endp, &flags))) { 313 month = tp->tm_mon + 1; 314 /* F_ISDAY is set only if a weekday was spelled out */ 315 /* F_ISDAY must be set if 0 < day < 8 */ 316 if ((day <= 7) && (day >= 1)) 317 interval = WEEKLY; 318 else 319 interval = MONTHLY; 320 } else if ((day <= 7) && (day >= 1)) 321 day += 10; 322 /* it's a weekday; make it the first one of the month */ 323 if (month == -1) { 324 month = tp->tm_mon + 1; 325 interval = MONTHLY; 326 } else { 327 if ((month > 12) || (month < 1)) 328 return (NULL); 329 if (calendar) 330 adjust_calendar(&day, &month); 331 } 332 } 333 334 /* 2. {Monthname} XYZ ... */ 335 else if (flags & F_ISMONTH) { 336 month = v1; 337 if (month == -1) { 338 month = tp->tm_mon + 1; 339 interval = MONTHLY; 340 } 341 /* Monthname {day,weekday} */ 342 /* if no recognizable day, assume the first day in month */ 343 if (!(day = getfield(endp, &endp, &flags))) 344 day = 1; 345 /* If a weekday was spelled out without an ordering, 346 * assume the first of that day in the month */ 347 if ((flags & F_ISDAY)) { 348 if ((day >= 1) && (day <=7)) 349 day += 10; 350 } else if (calendar) 351 adjust_calendar(&day, &month); 352 } 353 354 /* Hm ... */ 355 else { 356 v2 = getfield(endp, &endp, &flags); 357 358 /* 359 * {Day} {Monthname} ... 360 * where Day <= 12 361 */ 362 if (flags & F_ISMONTH) { 363 day = v1; 364 month = v2; 365 if (month == -1) { 366 month = tp->tm_mon + 1; 367 interval = MONTHLY; 368 } else if (calendar) 369 adjust_calendar(&day, &month); 370 } 371 372 /* {Month} {Weekday,Day} ... */ 373 else { 374 /* F_ISDAY set, v2 > 12, or no way to tell */ 375 month = v1; 376 if ((month > 12) || (month < 1)) 377 return (NULL); 378 /* if no recognizable day, assume the first */ 379 day = v2 ? v2 : 1; 380 if ((flags & F_ISDAY)) { 381 if ((day >= 1) && (day <= 7)) 382 day += 10; 383 } else 384 adjust_calendar(&day, &month); 385 } 386 } 387 388 /* convert Weekday into *next* Day, 389 * e.g.: 'Sunday' -> 22 390 * 'SundayLast' -> ?? 391 */ 392 if (flags & F_ISDAY) { 393 #if DEBUG 394 fprintf(stderr, "\nday: %d %s month %d\n", day, endp, month); 395 #endif 396 397 varp = 1; 398 /* variable weekday, SundayLast, MondayFirst ... */ 399 if (day < 0 || day >= 10) 400 vwd = day; 401 else { 402 day = tp->tm_mday + (((day - 1) - tp->tm_wday + 7) % 7); 403 interval = WEEKLY; 404 } 405 } else 406 /* Check for silliness. Note we still catch Feb 29 */ 407 if (!(flags & F_SPECIAL) && 408 (day > (cumdays[month + 1] - cumdays[month]) || day < 1)) { 409 if (!((month == 2 && day == 29) || 410 (interval == MONTHLY && day <= 31))) 411 return (NULL); 412 } 413 414 if (!(flags & F_SPECIAL)) { 415 monthp = month; 416 dayp = day; 417 day = cumdays[month] + day; 418 #if DEBUG 419 fprintf(stderr, "day2: day %d(%d) yday %d\n", dayp, day, tp->tm_yday); 420 #endif 421 /* Speed up processing for the most common situation: yearly events 422 * when the interval being checked is less than a month or so (this 423 * could be less than a year, but then we have to start worrying about 424 * leap years). Only one event can match, and it's easy to find. 425 * Note we can't check special events, because they can wander widely. 426 */ 427 if (((v1 = offset + f_dayAfter) < 50) && (interval == YEARLY)) { 428 memcpy(&tmtmp, tp, sizeof(struct tm)); 429 tmtmp.tm_mday = dayp; 430 tmtmp.tm_mon = monthp - 1; 431 if (vwd) { 432 /* We want the event next year if it's late now 433 * this year. The 50-day limit means we don't have to 434 * worry if next year is or isn't a leap year. 435 */ 436 if (tp->tm_yday > 300 && tmtmp.tm_mon <= 1) 437 variable_weekday(&vwd, tmtmp.tm_mon + 1, 438 tmtmp.tm_year + 1900 + 1); 439 else 440 variable_weekday(&vwd, tmtmp.tm_mon + 1, 441 tmtmp.tm_year + 1900); 442 day = cumdays[tmtmp.tm_mon + 1] + vwd; 443 tmtmp.tm_mday = vwd; 444 } 445 v2 = day - tp->tm_yday; 446 if ((v2 > v1) || (v2 < 0)) { 447 if ((v2 += isleap(tp->tm_year + 1900) ? 366 : 365) 448 <= v1) 449 tmtmp.tm_year++; 450 else if(!bodun || (day - tp->tm_yday) != -1) 451 return(NULL); 452 } 453 if ((tmp = malloc(sizeof(struct match))) == NULL) 454 err(1, NULL); 455 456 if (bodun && (day - tp->tm_yday) == -1) { 457 tmp->when = f_time - 1 * SECSPERDAY; 458 tmtmp.tm_mday++; 459 tmp->bodun = 1; 460 } else { 461 tmp->when = f_time + v2 * SECSPERDAY; 462 tmp->bodun = 0; 463 } 464 465 (void)mktime(&tmtmp); 466 fill_print_date(tmp, &tmtmp); 467 tmp->var = varp; 468 tmp->next = NULL; 469 return(tmp); 470 } 471 } else { 472 varp = 1; 473 /* Set up v1 to the event number and ... */ 474 v1 = vwd % (NUMEV + 1) - 1; 475 vwd /= (NUMEV + 1); 476 if (v1 < 0) { 477 v1 += NUMEV + 1; 478 vwd--; 479 } 480 dayp = monthp = 1; /* Why not */ 481 } 482 483 /* Compare to past and coming instances of the event. The i == 0 part 484 * of the loop corresponds to this specific instance. Note that we 485 * can leave things sort of higgledy-piggledy since a mktime() happens 486 * on this before anything gets printed. Also note that even though 487 * we've effectively gotten rid of f_dayBefore, we still have to check 488 * the one prior event for situations like "the 31st of every month" 489 * and "yearly" events which could happen twice in one year but not in 490 * the next */ 491 tmp2 = matches; 492 for (i = -1; i < 2; i++) { 493 memcpy(&tmtmp, tp, sizeof(struct tm)); 494 tmtmp.tm_mday = dayp; 495 tmtmp.tm_mon = month = monthp - 1; 496 do { 497 v2 = 0; 498 switch (interval) { 499 case WEEKLY: 500 tmtmp.tm_mday += 7 * i; 501 break; 502 case MONTHLY: 503 month += i; 504 tmtmp.tm_mon = month; 505 switch(tmtmp.tm_mon) { 506 case -1: 507 tmtmp.tm_mon = month = 11; 508 tmtmp.tm_year--; 509 break; 510 case 12: 511 tmtmp.tm_mon = month = 0; 512 tmtmp.tm_year++; 513 break; 514 } 515 if (vwd) { 516 v1 = vwd; 517 variable_weekday(&v1, tmtmp.tm_mon + 1, 518 tmtmp.tm_year + 1900); 519 tmtmp.tm_mday = v1; 520 } else 521 tmtmp.tm_mday = dayp; 522 break; 523 case YEARLY: 524 default: 525 tmtmp.tm_year += i; 526 if (flags & F_SPECIAL) { 527 tmtmp.tm_mon = 0; /* Gee, mktime() is nice */ 528 tmtmp.tm_mday = spev[v1].getev(tmtmp.tm_year + 529 1900) + vwd; 530 } else if (vwd) { 531 v1 = vwd; 532 variable_weekday(&v1, tmtmp.tm_mon + 1, 533 tmtmp.tm_year + 1900); 534 tmtmp.tm_mday = v1; 535 } else { 536 /* Need the following to keep Feb 29 from 537 * becoming Mar 1 */ 538 tmtmp.tm_mday = dayp; 539 tmtmp.tm_mon = monthp - 1; 540 } 541 break; 542 } 543 /* How many days apart are we */ 544 if ((ttmp = mktime(&tmtmp)) == -1) 545 warnx("time out of range: %s", endp); 546 else { 547 tdiff = difftime(ttmp, f_time)/ SECSPERDAY; 548 if (tdiff <= offset + f_dayAfter || 549 (bodun && tdiff == -1)) { 550 if (((tmtmp.tm_mon == month) || 551 (flags & F_SPECIAL) || 552 (interval == WEEKLY)) && 553 (tdiff >= 0 || 554 (bodun && tdiff == -1))) { 555 if ((tmp = malloc(sizeof(struct match))) == NULL) 556 err(1, NULL); 557 tmp->when = ttmp; 558 fill_print_date(tmp, &tmtmp); 559 tmp->bodun = bodun && tdiff == -1; 560 tmp->var = varp; 561 tmp->next = NULL; 562 if (tmp2) 563 tmp2->next = tmp; 564 else 565 matches = tmp; 566 tmp2 = tmp; 567 v2 = (i == 1) ? 1 : 0; 568 } 569 } else 570 i = 2; /* No point checking in the future */ 571 } 572 } while (v2 != 0); 573 } 574 return (matches); 575 } 576 577 578 int 579 getmonth(char *s) 580 { 581 char **p; 582 struct fixs *n; 583 584 for (n = fnmonths; n->name; ++n) 585 if (!strncasecmp(s, n->name, n->len)) 586 return ((n - fnmonths) + 1); 587 for (n = nmonths; n->name; ++n) 588 if (!strncasecmp(s, n->name, n->len)) 589 return ((n - nmonths) + 1); 590 for (p = months; *p; ++p) 591 if (!strncasecmp(s, *p, 3)) 592 return ((p - months) + 1); 593 return (0); 594 } 595 596 597 int 598 getday(char *s) 599 { 600 char **p; 601 struct fixs *n; 602 603 for (n = fndays; n->name; ++n) 604 if (!strncasecmp(s, n->name, n->len)) 605 return ((n - fndays) + 1); 606 for (n = ndays; n->name; ++n) 607 if (!strncasecmp(s, n->name, n->len)) 608 return ((n - ndays) + 1); 609 for (p = days; *p; ++p) 610 if (!strncasecmp(s, *p, 3)) 611 return ((p - days) + 1); 612 return (0); 613 } 614 615 /* return offset for variable weekdays 616 * -1 -> last weekday in month 617 * +1 -> first weekday in month 618 * ... etc ... 619 */ 620 int 621 getdayvar(char *s) 622 { 623 int offset; 624 625 626 offset = strlen(s); 627 628 /* Sun+1 or Wednesday-2 629 * ^ ^ */ 630 631 /* printf ("x: %s %s %d\n", s, s + offset - 2, offset); */ 632 switch(*(s + offset - 2)) { 633 case '-': 634 case '+': 635 return(atoi(s + offset - 2)); 636 break; 637 } 638 639 /* 640 * some aliases: last, first, second, third, fourth 641 */ 642 643 /* last */ 644 if (offset > 4 && !strcasecmp(s + offset - 4, "last")) 645 return(-1); 646 else if (offset > 5 && !strcasecmp(s + offset - 5, "first")) 647 return(+1); 648 else if (offset > 6 && !strcasecmp(s + offset - 6, "second")) 649 return(+2); 650 else if (offset > 5 && !strcasecmp(s + offset - 5, "third")) 651 return(+3); 652 else if (offset > 6 && !strcasecmp(s + offset - 6, "fourth")) 653 return(+4); 654 655 /* no offset detected */ 656 return(0); 657 } 658 659 660 int 661 foy(int year) 662 { 663 /* 0-6; what weekday Jan 1 is */ 664 year--; 665 return ((1 - year/100 + year/400 + (int)(365.25 * year)) % 7); 666 } 667 668 669 670 void 671 variable_weekday(int *day, int month, int year) 672 { 673 int v1, v2; 674 int *cumdays; 675 int day1; 676 677 if (isleap(year)) 678 cumdays = daytab[1]; 679 else 680 cumdays = daytab[0]; 681 day1 = foy(year); 682 /* negative offset; last, -4 .. -1 */ 683 if (*day < 0) { 684 v1 = *day/10 - 1; /* offset -4 ... -1 */ 685 *day = 10 + (*day % 10); /* day 1 ... 7 */ 686 687 /* which weekday the end of the month is (1-7) */ 688 v2 = (cumdays[month + 1] + day1) % 7 + 1; 689 690 /* and subtract enough days */ 691 *day = cumdays[month + 1] - cumdays[month] + 692 (v1 + 1) * 7 - (v2 - *day + 7) % 7; 693 #if DEBUG 694 fprintf(stderr, "\nMonth %d ends on weekday %d\n", month, v2); 695 #endif 696 } 697 698 /* first, second ... +1 ... +5 */ 699 else { 700 v1 = *day/10; /* offset */ 701 *day = *day % 10; 702 703 /* which weekday the first of the month is (1-7) */ 704 v2 = (cumdays[month] + 1 + day1) % 7 + 1; 705 706 /* and add enough days */ 707 *day = 1 + (v1 - 1) * 7 + (*day - v2 + 7) % 7; 708 #if DEBUG 709 fprintf(stderr, "\nMonth %d starts on weekday %d\n", month, v2); 710 #endif 711 } 712 } 713