/* quickcandles.com shipping schedule
 * Michaeljohn Clement
 * mj@mjclement.com
 * 2009-04-02
 * updated 2010-01-27
 */

;(function(){

/* ****************************************************** */
/* THE FOLLOWING DATES WILL NEED TO BE UPDATED EVERY YEAR */

/* All dates are in standard YYYY-MM-DD format. */

/* Daylight saving time currently starts, in the US, on the second Sunday of March, and ends on the first Sunday of November.  The operating system has a timezone database, but we don't have access to other timezones from JavaScript, so we include these dates directly.  The changeover happens between 2AM and 3AM (this is hardcoded in later). */

// see http://tf.nist.gov/general/history.htm#Anchor-16126
// and http://en.wikipedia.org/wiki/Daylight_saving_time_in_the_United_States#2007_to_the_present

var EDT_Start="2010-03-14"
  , EDT_End  ="2010-11-06"

/* Holidays */

/* The following are FedEx holidays which don't count as business days for the purposes of FedEx ground delivery times. */

/* These also need to be updated every year. */

,FedExHolidays=["2010-05-31" // Memorial Day
               ,"2010-07-05" // Independence Day (Sunday)
               ,"2010-09-06" // Labor Day
               ,"2010-11-25" // Thanksgiving
               ,"2010-12-24" // Christmas Eve (Friday)
               // Christmas and New Years Day fall on Saturdays, so they don't count anyway
               ,"2011-05-30" // Memorial Day
               ,"2011-07-04" // Independence Day
               ,"2011-09-05" // Labor Day
               ,"2011-11-24" // Thanksgiving
               ,"2011-12-23" // Christmas Eve (observed)
               ,"2011-12-30" // New Years Eve (obs.) (these are both Fridays, the 2011 Eves actually fall on Saturdays)
               ]

/* All of the above count as shipping holidays, but we also don't ship on any of the following days.  The difference is that once a package is already with FedEx, these days still count as business days towards FedEx delivery. */

/* These also need to be updated every year. */

,ExtraHolidays=["2010-04-02" // Good Friday
               ,"2011-04-22" // Good Friday
               ]

/* There are no more dates that need to be updated beyond this point. However there are a couple more variables you might change: */

,SameDayNote="Note: Ground Shipping will arrive in one business day, so there's no need to spend extra on 2nd Day Air or Next Day Air."
,TwoDayNote="Note: Ground Shipping will arrive in two business days, so there's no need to spend extra on 2nd Day Air."
,enterZip="Please enter a ZIP code."
,unknownZip="Sorry, we don't have any information on that ZIP code.  Please call us for delivery information."

/* The hour at which same-day shipping ends for the day, in 24h format, so 13 = 1:00PM, etc. */

,Cutoff=15

/* FedEx delivery days by first 3 digits of ZIP */
/* zip3 keys: leading 3 digits of ZIP, zeros stripped
      values: FedEx ground business days to that ZIP. */

/* This zip3 literal is 908 lines long, and is followed by the rest of the code.  There is nothing after this point that you are likely to want to edit. */

,zip3=
{5:2 // {{{
,10:2
,11:2
,12:2
,13:2
,14:2
,15:2
,16:2
,17:2
,18:2
,19:2
,20:2
,21:2
,22:2
,23:2
,24:2
,25:3
,26:2
,27:2
,28:2
,29:2
,30:2
,31:2
,32:2
,33:2
,34:2
,35:2
,36:2
,37:2
,38:2
,39:2
,40:2
,41:2
,42:2
,43:2
,44:3
,45:2
,46:3
,47:3
,48:3
,49:3
,50:2
,51:2
,52:2
,53:2
,54:2
,55:2
,56:2
,57:2
,58:2
,59:2
,60:2
,61:2
,62:2
,63:2
,64:2
,65:2
,66:2
,67:2
,68:2
,69:2
,70:2
,71:2
,72:2
,73:2
,74:2
,75:2
,76:2
,77:2
,78:2
,79:2
,80:2
,81:2
,82:2
,83:2
,84:2
,85:2
,86:2
,87:2
,88:2
,89:2
,100:2
,101:2
,102:2
,103:2
,104:2
,105:2
,106:2
,107:2
,108:2
,109:2
,110:2
,111:2
,112:2
,113:2
,114:2
,115:2
,116:2
,117:2
,118:2
,119:2
,120:2
,121:2
,122:2
,123:2
,124:2
,125:2
,126:2
,127:2
,128:3
,129:3
,130:3
,131:3
,132:3
,133:3
,134:3
,135:3
,136:3
,137:2
,138:2
,139:2
,140:2
,141:2
,142:2
,143:2
,144:2
,145:2
,146:2
,147:2
,148:3
,149:3
,150:2
,151:2
,152:2
,153:2
,154:2
,155:2
,156:2
,157:2
,158:2
,159:2
,160:2
,161:2
,162:2
,163:2
,164:2
,165:2
,166:2
,167:2
,168:2
,169:3
,170:2
,171:2
,172:2
,173:2
,174:2
,175:2
,176:2
,177:2
,178:2
,179:2
,180:2
,181:2
,182:2
,183:2
,184:2
,185:2
,186:2
,187:2
,188:2
,189:2
,190:2
,191:2
,192:2
,193:2
,194:2
,195:2
,196:2
,197:2
,198:2
,199:2
,200:2
,201:2
,202:2
,203:2
,204:2
,205:2
,206:2
,207:2
,208:2
,209:2
,210:2
,211:2
,212:2
,214:2
,215:2
,216:2
,217:2
,218:2
,219:2
,220:2
,221:2
,222:2
,223:2
,224:2
,225:2
,226:2
,227:2
,228:2
,229:2
,230:2
,231:2
,232:1
,233:2
,234:2
,235:2
,236:2
,237:2
,238:2
,239:1
,240:1
,241:1
,242:1
,243:1
,244:1
,245:1
,246:1
,247:1
,248:1
,249:1
,250:2
,251:2
,252:2
,253:2
,254:2
,255:2
,256:2
,257:2
,258:1
,259:1
,260:2
,261:2
,262:2
,263:2
,264:2
,265:2
,266:2
,267:2
,268:2
,270:1
,271:1
,272:1
,273:1
,274:1
,275:1
,276:1
,277:1
,278:1
,279:1
,280:1
,281:1
,282:1
,283:1
,284:1
,285:1
,286:1
,287:1
,288:1
,289:1
,290:1
,291:1
,292:1
,293:1
,294:1
,295:1
,296:1
,297:1
,298:1
,299:2
,300:1
,301:1
,302:1
,303:1
,304:2
,305:1
,306:1
,307:1
,308:1
,309:1
,310:1
,311:1
,312:1
,313:2
,314:2
,315:2
,316:2
,317:2
,318:1
,319:1
,320:2
,321:2
,322:2
,323:2
,324:2
,325:2
,326:2
,327:2
,328:2
,329:2
,330:2
,331:2
,332:2
,333:2
,334:2
,335:2
,336:2
,337:2
,338:2
,339:2
,341:2
,342:2
,344:2
,346:2
,347:2
,349:2
,350:2
,351:2
,352:1
,354:2
,355:2
,356:2
,357:2
,358:2
,359:1
,360:2
,361:1
,362:1
,363:2
,364:2
,365:2
,366:2
,367:2
,368:1
,369:2
,370:2
,371:2
,372:1
,373:2
,374:1
,375:2
,376:1
,377:1
,378:1
,379:1
,380:2
,381:2
,382:2
,383:2
,384:2
,385:1
,386:2
,387:2
,388:2
,389:2
,390:2
,391:2
,392:2
,393:2
,394:2
,395:2
,396:2
,397:2
,398:2
,399:1
,400:2
,401:2
,402:2
,403:1
,404:1
,405:1
,406:1
,407:1
,408:1
,409:1
,410:2
,411:2
,412:2
,413:1
,414:2
,415:1
,416:1
,417:1
,418:1
,420:2
,421:2
,422:2
,423:2
,424:2
,425:1
,426:1
,427:2
,430:2
,431:2
,432:2
,433:2
,434:2
,435:2
,436:2
,437:2
,438:2
,439:2
,440:2
,441:2
,442:2
,443:2
,444:2
,445:2
,446:2
,447:2
,448:2
,449:2
,450:2
,451:2
,452:2
,453:2
,454:2
,455:2
,456:2
,457:2
,458:2
,459:2
,460:2
,461:2
,462:2
,463:2
,464:2
,465:2
,466:2
,467:2
,468:2
,469:2
,470:2
,471:2
,472:2
,473:2
,474:2
,475:2
,476:2
,477:2
,478:2
,479:2
,480:2
,481:2
,482:2
,483:2
,484:2
,485:2
,486:2
,487:2
,488:2
,489:2
,490:2
,491:2
,492:2
,493:2
,494:2
,495:2
,496:2
,497:2
,498:3
,499:3
,500:3
,501:3
,502:3
,503:2
,504:3
,505:3
,506:3
,507:3
,508:2
,509:2
,510:3
,511:3
,512:3
,513:3
,514:3
,515:2
,516:2
,520:3
,521:3
,522:3
,523:3
,524:3
,525:3
,526:3
,527:2
,528:2
,530:3
,531:3
,532:3
,534:3
,535:3
,537:3
,538:3
,539:3
,540:3
,541:3
,542:3
,543:3
,544:3
,545:3
,546:3
,547:3
,548:3
,549:3
,550:3
,551:3
,553:3
,554:3
,555:3
,556:3
,557:3
,558:3
,559:3
,560:3
,561:3
,562:3
,563:3
,564:3
,565:3
,566:3
,567:3
,570:3
,571:3
,572:3
,573:3
,574:3
,575:3
,576:4
,577:3
,580:3
,581:3
,582:3
,583:4
,584:4
,585:4
,586:4
,587:4
,588:4
,590:4
,591:4
,592:4
,593:4
,594:4
,595:4
,596:4
,597:4
,598:4
,599:4
,600:2
,601:2
,602:2
,603:2
,604:2
,605:2
,606:2
,607:2
,608:2
,609:2
,610:3
,611:2
,612:2
,613:2
,614:2
,615:2
,616:2
,617:2
,618:2
,619:2
,620:2
,622:2
,623:2
,624:2
,625:2
,626:2
,627:2
,628:2
,629:2
,630:2
,631:2
,633:2
,634:2
,635:2
,636:2
,637:2
,638:2
,639:2
,640:2
,641:2
,644:2
,645:2
,646:2
,647:2
,648:2
,649:2
,650:2
,651:2
,652:2
,653:2
,654:2
,655:2
,656:2
,657:2
,658:2
,660:2
,661:2
,662:2
,664:2
,665:2
,666:2
,667:2
,668:2
,669:2
,670:3
,671:3
,672:2
,673:2
,674:2
,675:3
,676:3
,677:3
,678:3
,679:3
,680:3
,681:2
,683:3
,684:3
,685:2
,686:3
,687:3
,688:3
,689:3
,690:3
,691:3
,692:3
,693:3
,700:2
,701:2
,703:2
,704:2
,705:3
,706:3
,707:2
,708:2
,710:2
,711:2
,712:2
,713:2
,714:3
,716:2
,717:2
,718:2
,719:2
,720:2
,721:2
,722:2
,723:2
,724:2
,725:2
,726:2
,727:3
,728:2
,729:3
,730:3
,731:3
,733:3
,734:3
,735:3
,736:3
,737:3
,738:3
,739:3
,740:3
,741:3
,743:3
,744:3
,745:3
,746:3
,747:3
,748:3
,749:3
,750:3
,751:3
,752:3
,753:3
,754:3
,755:2
,756:3
,757:3
,758:3
,759:3
,760:3
,761:3
,762:3
,763:3
,764:3
,765:3
,766:3
,767:3
,768:3
,769:3
,770:3
,772:3
,773:3
,774:3
,775:3
,776:3
,777:3
,778:3
,779:3
,780:3
,781:3
,782:3
,783:3
,784:3
,785:4
,786:3
,787:3
,788:3
,789:3
,790:3
,791:3
,792:3
,793:3
,794:3
,795:3
,796:3
,797:3
,798:4
,799:4
,800:3
,801:3
,802:3
,803:3
,804:3
,805:3
,806:3
,807:3
,808:3
,809:3
,810:3
,811:4
,812:3
,813:4
,814:3
,815:3
,816:4
,820:3
,821:4
,822:3
,823:4
,824:4
,825:3
,826:3
,827:4
,828:4
,829:4
,830:4
,831:4
,832:4
,833:4
,834:4
,835:5
,836:4
,837:4
,838:5
,840:4
,841:4
,842:4
,843:4
,844:4
,845:4
,846:4
,847:4
,850:4
,851:4
,852:4
,853:4
,855:4
,856:4
,857:4
,859:4
,860:4
,863:4
,864:4
,865:4
,870:4
,871:3
,873:4
,874:4
,875:4
,877:3
,878:4
,879:4
,880:4
,881:3
,882:3
,883:4
,884:3
,885:4
,890:5
,891:4
,893:4
,894:4
,895:4
,897:4
,898:4
,900:4
,901:4
,902:4
,903:4
,904:4
,905:4
,906:4
,907:4
,908:4
,910:4
,911:4
,912:4
,913:4
,914:4
,915:4
,916:4
,917:4
,918:4
,919:4
,920:4
,921:4
,922:4
,923:4
,924:4
,925:4
,926:4
,927:4
,928:4
,930:4
,931:4
,932:4
,933:4
,934:4
,935:5
,936:4
,937:4
,938:4
,939:4
,940:4
,941:4
,942:4
,943:4
,944:4
,945:4
,946:4
,947:4
,948:4
,949:4
,950:4
,951:4
,952:4
,953:4
,954:4
,955:4
,956:4
,957:4
,958:4
,959:4
,960:5
,961:5
,967:8
,968:7
,970:5
,971:5
,972:5
,973:5
,974:5
,975:5
,976:5
,977:5
,978:5
,979:5
,980:5
,981:5
,982:5
,983:5
,984:5
,985:5
,986:5
,988:5
,989:5
,990:5
,991:5
,992:4
,993:5
,994:5
,995:8
,996:8
,997:7
,998:8
,999:8} // }}}


/* --------- */

/* Mozilla Array compat for down-level browsers */
if(!Array.prototype.map){
 Array.prototype.map=function(f){
  var i,l=this.length>>>0,res=new Array(l),thisp=arguments[1];
  if (typeof f != "function") throw new TypeError();
  for (i=0;i<l;i++)
   if (i in this)
    res[i] = f.call(thisp, this[i], i, this);
  return res}}

if(!Array.prototype.some){
 Array.prototype.some=function(f){
  var i=0,l=this.length,thisp=arguments[1]
  if(typeof f != "function")throw new TypeError();
  for (;i<l;i++){
   if(i in this
   && f.call(thisp,this[i],i,this))
    return true}
  return false}}

/* A simple calendar day with year, month, and day fields.
Note there is no January=0 brain damage here; months are numbered from 1. */
function CalendarDate(y,m,d){
 return {y:+y,m:+m,d:+d}}

function sameDay(a,b){
 return (a.y==b.y && a.m==b.m && a.d==b.d)}

function nextDay(a,b){
 var utcA=Date.UTC(a.y,a.m-1,a.d)
   , utcB=Date.UTC(b.y,b.m-1,b.d)
 return (utcB-utcA == 86400000)}

function weekDayOf(cd){
 return (new Date(Date.UTC(cd.y,cd.m-1,cd.d))).getUTCDay()}

function addDay(cd){
 if(cd.y > 2200)throw new Error('date too large') // break loops if something breaks
 var utc=Date.UTC(cd.y,cd.m-1,cd.d) + 86400000 // ms/day
   , d2=new Date(utc)
 return CalendarDate(d2.getUTCFullYear(),d2.getUTCMonth()+1,d2.getUTCDate())}

function parseISODate(s){
 var re=/(\d{4})-(\d\d)-(\d\d)/,m
 if(!(m=re.exec(s)))throw new Error('parseISODate: '+s)
 return CalendarDate(m[1],m[2],m[3])}

function isWeekend(d){var
 day=(new Date(d.y,d.m-1,d.d)).getDay()
 return (day==0 || day==6)}

FedExH=FedExHolidays.map(parseISODate)
ExtraH=ExtraHolidays.map(parseISODate)

/* EST changes to or from EDT at 07:00 UTC on the given day.  (i.e. 2AM or 3AM local time changes to the other).  EST is UTC-5, EDT is UTC-4. */
function EDT_fixup(s){
 var day=parseISODate(s)
 return Date.UTC(day.y,day.m-1,day.d,7,0,0)}

EDT_Start=EDT_fixup(EDT_Start)
EDT_End  =EDT_fixup(EDT_End)

var now=new Date() // current time

var now_ms= +now // current time in ms since the epoch

function isEasternDST(utc){return utc>EDT_Start && utc<EDT_End}

/* A shipping day is any day other than FedEx or Extra holidays or weekends. */
function shippingDay(d){
 if(isWeekend(d)) return false
 if(FedExH
   .concat(ExtraH)
   .some(function(d2){return sameDay(d,d2)})) return false
 return true}

function fedExDay(d){
 if(isWeekend(d)) return false
 if(FedExH
   .some(function(d2){return sameDay(d,d2)})) return false
 return true}

function shipDate(sd){
 while(!shippingDay(sd)) sd=addDay(sd)
 return sd}

/* The remaining time before the shipping cutoff on a given day, in ms */
function remainingShipDateTime(d,now){
 var utcNow= +now
   , ET2PM=Cutoff-(isEasternDST(Date.UTC(d.y,d.m-1,d.d,12))?-4:-5)
   , utcThen=Date.UTC(d.y,d.m-1,d.d,ET2PM,0,0)
 return utcThen-utcNow}

/* Calendar day of a given time in America/New_York, or the next day if after the shipping cutoff time there. */
function easternDay(d){
 var utc=d.valueOf()
   , offset=24 - Cutoff + (isEasternDST(utc) ? -4 : -5) //jump forward from the cutoff to 24:00, i.e. the next day
   , utc2=new Date(utc+offset*3600000)
 return CalendarDate(utc2.getUTCFullYear(),utc2.getUTCMonth()+1,utc2.getUTCDate())}

/* The FedEx Ground delivery date, from ZIP code table, counting that number of business days out from the ship date. */
function groundDeliveryDate(sd,zip){
 var key=parseInt(zip.slice(0,3),10)
   , days=zip3[key]
 if(!days)throw new Error('unknown ZIP '+zip)
 return addFedExDays(sd,days)}

function twoDayDeliveryDate(sd){
 return addFedExDays(sd,2)}

function overnightDeliveryDate(sd){
 return addFedExDays(sd,1)}

function nextFedExDay(d){
 d=addDay(d)
 while(!fedExDay(d)) d=addDay(d)
 return d}

function addFedExDays(d,n){
 while(n--) d=nextFedExDay(d)
 return d}

/* ----------
 * Build Text
 * ----------
 * Here is where we construct the text the user sees.
 */

var shortDays=['Sun','Mon','Tue','Wed','Thu','Fri','Sat']
  , longDays=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']
  , fullMonths=['January','February','March','April','May','June','July','August','September','October','November','December']

function twoCh(n){return (n>10?'':'0')+n}

function shortUTCDate(utc){
 return shortDays[utc.getUTCDay()]
      + ', '
      + fullMonths[utc.getUTCMonth()]
      + ' '
      + utc.getUTCDate()}

function shortUTCTime(utc){
 return (utc.getUTCHours() % 12 || 12)
      + ':'
      + twoCh(utc.getUTCMinutes())
      + (utc.getUTCHours()<12 ?' AM' :' PM')}

function formatCurrentET(d){
 var offset=isEasternDST(+d) ? -4 : -5
   , utc=new Date(+d + offset * 3600000)
 return shortUTCDate(utc)
      + ', '
      + shortUTCTime(utc)}

function friendlyDay(date,today){
 if(sameDay(date,today)) return 'today'
 if(nextDay(today,date)) return 'tomorrow'
 return shortDays[weekDayOf(date)]}

function friendlyTimeSpan(ms){
 var h=Math.floor(ms/3600000)
   , m=Math.floor((ms%3600000)/60000)
 return (h ?h+' hours' :'')
      + (h && m ?' and ' :'')
      + ((!h || m) ?m +' minutes' :'')}

function formatShipDate(ships,today){
 return friendlyDay(ships,today)
      + ', '
      + fullMonths[ships.m-1]
      + ' '
      + ships.d
      + ', '
      + ships.y}

function formatRemaining(now,ships){
 var rem=remainingShipDateTime(ships,now)
 return (rem>86400000)
        ?''
        :('('+friendlyTimeSpan(rem)+' from now)')}

function formatShipDate2(ships,today){
 if(sameDay(today,ships)) return 'today'
 if(nextDay(today,ships)) return 'tomorrow'
 return 'on that day'}

/* EST or EDT as appropriate for afternoon of the given day */
function formatShipDayTimezone(ships){
 return isEasternDST(Date.UTC(ships.y,ships.m-1,ships.d,18))
         ?'EDT'
         :'EST'}

function formatDelivery(day){
 return shortDays[weekDayOf(day)]
      + ', '
      + fullMonths[day.m-1]
      + ' '
      + day.d}

function formatNote(ground,twoday,overnight){
 if(sameDay(ground,overnight))
  return SameDayNote
 if(sameDay(ground,twoday))
  return TwoDayNote
 return ''}

function buildText(now,zip){
 var day=easternDay(now)
   , localDay=CalendarDate(now.getFullYear()
                          ,now.getMonth()+1
                          ,now.getDate())
   , ships=shipDate(day)
   , ground=groundDeliveryDate(ships,zip)
   , twoday=twoDayDeliveryDate(ships,zip)
   , overnight=overnightDeliveryDate(ships,zip)
 return (
  {ship_currentET:formatCurrentET(now)
  ,ship_easternTimezone:formatShipDayTimezone(ships)
  ,ship_shipOn:formatShipDate(ships,localDay)
  ,ship_remaining:formatRemaining(now,ships)
  ,ship_shipOn2:formatShipDate2(ships,localDay)
  ,ship_delivery_overnight:formatDelivery(overnight)
  ,ship_delivery_twoday:formatDelivery(twoday)
  ,ship_delivery_ground:formatDelivery(ground)
  ,ship_delivery_note:formatNote(ground,twoday,overnight)
  })}

/* ----------------
 * Text replacement
 * ----------------
 * We swap the generated text into the page
 */

function setElementsText(o){
 var p,el
 for(p in o){
  el=document.getElementById(p)
  if(!el) continue
  el.innerHTML=''
  el.appendChild(document.createTextNode(o[p]/*+' '*/))}} // +' ' to deal with IE6 whitespace bug

/* -----
 * Setup 
 * -----
 * Here we attach everything to the DOM
 */

var show
  , info
  , close
  , zipInput
  , timeout
  , error
  , output
  , zipCookie

if (window.addEventListener) addEventListener('load',load,false)
else if (window.attachEvent) attachEvent('onload',load)

function load(){
 show=document.getElementById('shipping_schedule')
 info=document.getElementById('shipping_info')
 close=document.getElementById('shipping_close')
 error=document.getElementById('shipping_error')
 output=document.getElementById('shipping_output')
 zipInput=document.getElementById('ZIP')
 zipInput.onkeydown=zipInput.onkeyup=zipInput.onclick=handler
 close.onclick=show.onclick=toggle
 zipCookie=getCookie()}

function handler(){
 if(timeout) clearTimeout(timeout)
 timeout=setTimeout(update,200)}

function toggle(){
 if(info.style.display=='block'){
  info.style.display='none'
  return}
 info.style.display='block'
 if(zipCookie && !zipInput.value.length)
  zipInput.value=zipCookie
 zipInput.focus()
 update()}

function showError(s){
 error.innerHTML=s
 error.style.display='block'
 output.style.display='none'}

function clearError(){
 error.style.display='none'
 output.style.display='block'}

function update(){
 var text,zip=zipInput.value
 if(timeout) clearTimeout(timeout)
 timeout=setTimeout(update,10000)
 if(!/^\d{5}$/.test(zip))
  return showError(enterZip)
 if(!(parseInt(zip.slice(0,3),10) in zip3))
  return showError(unknownZip)
 if(zip != zipCookie) setCookie(zip)
 clearError()
 text=buildText(new Date(),zip)
 setElementsText(text)}

function setCookie(zip){
 zipCookie=zip
 document.cookie='zip='+zip+';path=/;expires='
  + ((new Date(+(new Date()) + 7*86400000)).toGMTString())}

function getCookie(){
 var all=document.cookie
   , m=all.match(/zip=(\d{5})/)
 return m ?m[1] :''}

})();
