Using ColdFusion to Create an Ebay-esque Auction Countdown Timer Custom Tag
The other day I read one of Ben Nadel's "Ask Ben" posts where a reader asked about calculating the difference between two dates and wanted to know how to display the estimated time left to buy ... much like ebay. Ben did a great job of giving a brief explanation on calculating the difference between two dates and kind of left it at that.
This sounded interesting to me and I couldn't resist the challenge to create a ColdFusion custom tag to handle the calculations and even display (to a limited degree of course). So here's what I came up with:
<cfsetting enablecfoutputonly="yes">
<!---
Document: /extensions/customtags/auctiontimer.cfm
Author: Steve Withington | www.stephenwithington.com
Creation Date: August 15, 2008
Copyright: (c) 2008 Stephen Withington | www.stephenwithington.com
Purpose: Outputs an ebay-esque count down timer.
Instructions: Place this template in your custom tags folder. Simply call the custom tag using
whatever method you're comfortable with and pass in at least a valid date for the
ATTRIBUTES.dateEnd variable.
Accepts 2 ATTRIBUTES:
1) dateStart (optional, DATETIME)
2) dateEnd (optional, DATETIME) - typically passed in with a query DATETIME value.
Example Usage: <cf_auctiontimer dateEnd="8/15/2008 9:40 AM">
Revision Log:
12/4/2008 - sjw - Added code to a) ensure the dateStart occurs prior to dateEnd and b) if dateStart has
already passed, then adjust dateStart to now().
05/14/2010 - sjw - Hopefully squashed an annoying bug with the 'hours2go' var.
--->
<cfsetting enablecfoutputonly="no">
<!--- if defined <style>'s haven't been output yet --->
<cfif NOT isDefined("REQUEST.localStyle")>
<style type="text/css">
<!--
.boldred {
font-weight: bold;
color: #FF0000;
}
-->
</style>
<!--- remember the <style>'s have been output so we don't output it again --->
<cfset REQUEST.localStyle = "true" />
</cfif>
<cfsetting enablecfoutputonly="yes">
<cfparam name="ATTRIBUTES.dateStart" default="#now()#" />
<cfparam name="ATTRIBUTES.dateEnd" default="#now()#" />
<!--- I prefer to throw a friendly error message instead of using 'type' in the cfparams for this tag --->
<cfif NOT isValid("date", ATTRIBUTES.dateStart)>
<cfthrow message="dateStart must be a valid DATE" detail="The value #ATTRIBUTES.dateStart# is not a valid DATE. For example, #dateformat(now(), 'mm/dd/yyyy')# #timeformat(now(), 'h:mm:ss tt')# is a valid DATETIME value." />
</cfif>
<cfif NOT isValid("date", ATTRIBUTES.dateEnd)>
<cfthrow message="dateEnd must be a valid DATE" detail="The value #ATTRIBUTES.dateEnd# is not a valid DATE. For example, #dateformat(now(), 'mm/dd/yyyy')# #timeformat(now(), 'h:mm:ss tt')# is a valid DATETIME value." />
</cfif>
<!--- if the start date is greater than the end date, throw an error --->
<cfset compareGivenDates = DateCompare(ATTRIBUTES.dateStart, ATTRIBUTES.dateEnd, "s")>
<cfif compareGivenDates eq 1>
<cfthrow message="Ooops! dateStart must occur before dateEnd." detail="The value #ATTRIBUTES.dateStart# should be a valid DATE that occurs before #ATTRIBUTES.dateEnd#." />
</cfif>
<!--- if the start date has already past, if so, then start date should be rightNow --->
<cfset rightNow = dateformat(now(), "mm/dd/yyyy") & " " & timeformat(now(), "hh:mm:ss tt") />
<cfset compareNow = DateCompare(rightNow, ATTRIBUTES.dateStart, "s") />
<cfswitch expression="#compareNow#">
<cfcase value="-1">
<cfthrow message="Bidding hasn't started yet!" detail="Whoa there buddy ... the bidding will open soon. Come back later, ok?" />
</cfcase>
<cfcase value="1">
<cfset ATTRIBUTES.dateStart = rightNow />
</cfcase>
</cfswitch>
<!--- scope the final return variable --->
<cfset returnTimeRemaining="" />
<cfset dateStart = dateformat(ATTRIBUTES.dateStart, "mm/dd/yyyy") & " " & timeformat(ATTRIBUTES.dateStart, "hh:mm:ss tt") />
<cfset dateEnd = dateformat(ATTRIBUTES.dateEnd, "mm/dd/yyyy") & " " & timeformat(ATTRIBUTES.dateEnd, "hh:mm:ss tt") />
<cfset hdif = Abs(DateDiff("h", dateEnd, dateStart)) />
<cfset ndif = Abs(DateDiff("n", dateEnd, dateStart)) />
<cfset sdif = Abs(DateDiff("s", dateEnd, dateStart)) />
<cfset years2go = Abs(DateDiff("yyyy", dateEnd, dateStart)) />
<cfset months2go = Abs(DateDiff("m", dateEnd, dateStart)) />
<cfset weeks2go = Abs(DateDiff("ww", dateEnd, dateStart)) />
<cfset days2go = Abs(DateDiff("d", dateEnd, dateStart)) />
<cfif datepart('h', now()) lt 12 or days2go eq 1>
<cfset h = 'h' />
<cfelse>
<cfset h = 'H' />
</cfif>
<cfset local.hours2go = TimeFormat(dateEnd-dateStart, h) />
<cfset min2go = TimeFormat("#dateEnd-dateStart#", "m") />
<cfset sec2go = TimeFormat("#dateEnd-dateStart#", "s") />
<!--- some modified calculations are needed if there is more than 1 month to go --->
<cfset newmonths = months2go-(years2go*12) />
<cfset tempDate = dateadd("m", months2go, ATTRIBUTES.dateStart) />
<cfset newweeks = Abs(DateDiff("ww", ATTRIBUTES.dateEnd, tempDate)) />
<cfset tempdays = Abs(DateDiff("d", ATTRIBUTES.dateEnd, tempDate)) />
<cfset newdays = tempdays-(newweeks*7) />
<!--- compare the dateStart to dateEnd down to the SECOND --->
<cfset comparison = DateCompare(dateStart, dateEnd, "s") />
<cfswitch expression="#comparison#">
<!--- Time still remaining --->
<cfcase value="-1">
<!--- YEARS TO GO --->
<cfif years2go GT 1>
<cfset returnTimeRemaining = returnTimeRemaining & "#years2go#y #newmonths#m #newweeks#w #newdays#d #hours2go#h #min2go#m #sec2go#s" />
<!--- MONTHS TO GO --->
<cfelseif months2go GT 1>
<cfset returnTimeRemaining = returnTimeRemaining & "#months2go#m #newweeks#w #newdays#d #hours2go#h #min2go#m #sec2go#s" />
<!--- WEEKS TO GO --->
<cfelseif weeks2go GT 1>
<!---<cfset newdays = days2go-(weeks2go*7) />--->
<cfset returnTimeRemaining = returnTimeRemaining & "#weeks2go#w #newdays#d #hours2go#h #min2go#m #sec2go#s" />
<!--- DAYS TO GO --->
<cfelseif hdif GT 24>
<cfset returnTimeRemaining = returnTimeRemaining & "#days2go#d #hours2go#h #min2go#m #sec2go#s" />
<!--- HOURS TO GO --->
<cfelseif ndif GT 60>
<cfset returnTimeRemaining = returnTimeRemaining & "#hours2go#h #min2go#m #sec2go#s" />
<!--- MINUTES TO GO --->
<cfelseif sdif GT 60>
<cfset returnTimeRemaining = returnTimeRemaining & "<span class=""boldred"">#min2go#m #sec2go#s</span>" />
<!--- SECONDS TO GO --->
<cfelseif sdif GT 01>
<cfset returnTimeRemaining = returnTimeRemaining & "<span class=""boldred"">< 1m</span> <em>or #sec2go#s to be exact.</em>" />
<!--- TIME HAS ENDED --->
<cfelse>
<cfset returnTimeRemaining = "<span class=""boldred"">Time has ended.</span>" />
</cfif>
</cfcase>
<!--- Times are the same. --->
<cfcase value="0">
<cfset returnTimeRemaining = "<span class=""boldred"">Ding!</span> It's over … you seriously just missed it!" />
</cfcase>
<!--- Time has expired. --->
<cfcase value="1">
<cfset returnTimeRemaining = "<strong>Bidding has ended for this item.</strong>" />
</cfcase>
</cfswitch>
<!--- FINAL OUTPUT --->
<cfoutput>#returnTimeRemaining#</cfoutput>
<cfsetting enablecfoutputonly="no">
There's several calculations going on in there, but ultimately the custom tag can accept two parameters, dateStart and dateEnd. They both default to the current date and time, so if you don't pass anything to it, the countdown would simply reply with "Bidding has ended for this item." The idea though would be to just pass in the ending date (dateEnd) and as long as the ending date is in the future, then a countdown is displayed and formatted quite similar to the one found on ebay.
Assuming you've dropped the code above into a template and placed it into your custom tags folder and named it "auctiontimer.cfm", you could add the following to a page and see the results:
<cfset myQuery.DisplayEndDate = "10/08/2008 2:30 PM" />
<p>Time Left: <cf_auctiontimer dateEnd="#myQuery.DisplayEndDate#"></p>
Would display "Time Left: 7w 2d 12h 39m 45s" - (assuming the page was executed at approximately 1:50 PM on 08/15/2008 that is).
Just for giggles, I created a little more sophisticated example using a query for newbie's trying to figure this stuff out:
<html>
<head>
<title>ebay-esque Auction Countdown Timer: A More Dynamic Example</title>
<style type="text/css">
<!--
body {
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
}
#container {
clear: both;
float: left;
width: 535px;
padding: 10px;
}
.itemWrapper {
clear: both;
float: left;
width: 525px;
padding: 5px;
border-bottom: 1px solid #666;
}
.itemTitle {
float: left;
width: 250px;
}
.itemPrice {
float: left;
width: 90px;
text-align: right;
}
.itemEndDate {
float: right;
width: 175px;
text-align: right;
}
-->
</style>
</head>
<body>
<!--- a simple query for testing purposes only --->
<cfset q = queryNew("ItemTitle, ItemDesc, ItemPrice, DisplayEndDate", "VarChar, VarChar, Decimal, Date") />
<cfset newRow = queryAddRow(q, 2) />
<cfset temp = querySetCell(q, "ItemTitle", "1969 Fender Strat", 1) />
<cfset temp = querySetCell(q, "ItemDesc", "MINT CONDITION. This baby is a screamer. Seymour Duncan Pickups, and MORE!", 1) />
<cfset temp = querySetCell(q, "ItemPrice", "8100.69", 1) />
<cfset temp = querySetCell(q, "DisplayEndDate", "09/16/2008 2:08:37 AM", 1) />
<cfset temp = querySetCell(q, "ItemTitle", "Rusty Bicycle ", 2) />
<cfset temp = querySetCell(q, "ItemDesc", "If you like rust, then this is the bike for you. Left in yard for 20 years.", 2) />
<cfset temp = querySetCell(q, "ItemPrice", "8.00", 2) />
<cfset temp = querySetCell(q, "DisplayEndDate", "10/08/2020 2:19:00 PM", 2) />
<div id="container">
<div class="itemWrapper">
<div class="itemTitle"><h3>Item Title</h3></div>
<div class="itemPrice"><h3>Price</h3></div>
<div class="itemEndDate"><h3>Time Left</h3></div>
</div>
<cfoutput query="q">
<div class="itemWrapper">
<div class="itemTitle"><a href="javascript:void(0);">#ItemTitle#</a><br />
#ItemDesc#</div>
<div class="itemPrice"><strong>#dollarformat(ItemPrice)#</strong></div>
<div class="itemEndDate"><cf_auctiontimer dateEnd="#DisplayEndDate#"></div>
</div>
</cfoutput>
</div>
</body>
</html>
This would display something like:
So there you have it, an ebay-esque auction countdown timer. There are obviously several other ways to accomplish this but I wanted to have a little fun. Enjoy!
Comments
Nice job man! Way to take the concept and really flesh it out into something usable.
Thanks, I needed a break and thought this would be fun. (Which, of course, it was.)
<cfparam name="ATTRIBUTES.dateStart" default="#DateFormat(now(), 'dd/mm/yyyy')#" />
<cfparam name="ATTRIBUTES.dateEnd" default="#DateFormat(now(), 'dd/mm/yyyy')#" />
I copied your code and saved as auctiontimer.cfm in the customTags directory. I am running cfmx7
When I did the following I should have gotten back 1 day but instead I got 12 hours.
<cf_auctiontimer dateStart="12/21/2008 8:39 am" dateEnd="12/2/2008 8:39 am">
Here are the results:
12h 0m 0s
Any help is appreciated.
Nice catch! I totally missed this (obviously). I'll update the post to account for it but here's the code needed to fix the custom tag:
<!--- if the start date has already past, if so, then start date should be rightNow --->
<cfset rightNow = dateformat(now(), "mm/dd/yyyy") & " " & timeformat(now(), "hh:mm:ss tt") />
<cfset compareNow = DateCompare(rightNow, ATTRIBUTES.dateStart, "s") />
<cfswitch expression="#compareNow#">
<cfcase value="-1">
<cfthrow message="Bidding hasn't started yet!" detail="Whoa there buddy ... the bidding will open soon. Come back later, ok?" />
</cfcase>
<cfcase value="1">
<cfset ATTRIBUTES.dateStart = rightNow />
</cfcase>
</cfswitch>
Place it right above this line of code:
<!--- scope the final return variable --->
<cfset returnTimeRemaining="" />
The revised code will throw an error if the startDate isn't here yet, alerting you that bidding hasn't even started yet. Then, if the startDate has come (and past), then we need to adjust the timer to calculate the actual time remaining which would be the difference between Now() and the dateEnd.
Thanks for catching this! I'm also throwing in some additional code in my example to ensure the start date occurs before the end date too for good measure.
Let me know if you find anything else.
This is a great tag. Exactly what I have been looking for. It is not calculating properly for me if the time is between 12 and 24 hours...it is just defaulting to 12 hours.
The following is showing up as 8h 48m 28s instead of 20h 48m 28s
The current date/time is 3/26/2009 1:45 PM
<cfset myQuery.DisplayEndDate = "3/27/2009 10:35 AM" />
Am I doing something wrong?
Thanks again for the great post and info :)
Tom
Sorry for the delay in answering your question, I must have missed it somehow.
Anyway, if you prefer to use a 24 hour clock, I believe all you should have to do is alter any of the timeformats from "hh:" to "HH:"
And since you would be using the 24-hour clock, you would also want to remove any "tt" from the timeformats.
The only way I could fix it was to insert the following;
<cfset ddif = Abs(DateDiff("d", dateEnd, dateStart)) />
and change the days2go and hours2go cfsets to:
<cfset days2go = ddif>
<cfset hours2go = hdif - (ddif * 24)>
This fixed 'er right up.
elegant piece of code none the less.
Cheers,
Duane
I think I see what's happening now. Your method is buggy too depending on the time of day ... it works if it's after 12pm, it breaks if it's before then.
Try replacing the 'hours2go' variable with this instead:
<cfif datepart('h', now()) lt 12>
<cfset h = 'h' />
<cfelse>
<cfset h = 'H' />
</cfif>
<cfset local.hours2go = TimeFormat(dateEnd-dateStart, h) />
There still seems to be a problem if the days2go is 1 and the hours2go is 0. The result will display as "0h 3m 46s" - from now (May 14, 2010 3:28:49 PM) to May 15, 2010 3:32:35 PM. It should return "1d 0h 3m 46s".
Ideas?
We'll get this figured out one way or another! (and to think I originally wrote this nearly 2 years ago now).
All right man, let's try this:
<cfif datepart('h', now()) lt 12 or days2go eq 1>
<cfset h = 'h' />
<cfelse>
<cfset h = 'H' />
</cfif>
Great script!
Can you assist me.
I am trying to create an auction application similar to www.smokoo.co.za where the timer counts down in real time.
Do you have a script that would be able to perform that functionality?
Thanks
The timer you're looking for is using JavaScript. You could use the script I have here to get the remaining time and then pass that information to JS to let it do its thing. As for the javascript itself, you could easily inspect the site you referred me to and take a look.
http://stackoverflow.com/questions/24842248/date-t...