Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with:

Programmatically Completing A List Of Movie Showtimes In ColdFusion

By Ben Nadel on
Tags: ColdFusion

If you look at the movie showtimes on Google's movie showtimes pages, you'll quickly see that their showtimes are only partially populated. That is, they always show you the Hours and Minutes of the showtimes; but, the AM/PM portion is missing from the majority of listings. While it might seem like an easy task to mentally add the AM/PM values yourself, I have found this to be quite challenging from a programmatic standpoint. But, after many failed attempts, I finally figured how to take a list of showtimes and complete it using ColdFusion.

To demonstrate what I am talking about, imagine that you were given the following list of movie showtimes:

  • 10:00 11:55AM 3:00 7:00 10:00
  • 2:00PM 5:00 11:00 12:30 2:00AM
  • 10:00AM 11:50PM 1:30AM

Then, you had to take those showtimes, and programmatically add the AM/PM parts to each individual time such that you could return the following lists:

  • 10:00AM 11:55AM 3:00PM 7:00PM 10:00PM
  • 2:00PM 5:00PM 11:00PM 12:30AM 2:00AM
  • 10:00AM 11:50PM 1:30AM

As you can see, there doesn't seem to be much rhyme or reason to where the "known" AM/PM values are. Sometimes they are on the first item; sometimes they are on the last item; sometimes they are right before an AM-PM cross-over; sometimes they are right after an AM-PM cross-over. The only thing that we really do know is that each list will contain at least one known AM/PM value.

After approaching this problem from a few different angles, I finally decided to just assume that I know the very first AM/PM value (which may be wrong); then, based on that assumption, I can walk through the list of showtimes, adding AM/PM values as I go. If I ever encounter a "known" AM/PM value that is different from the one the I am about to add, it simply indicates that my initial assumption was wrong and tells me that my AM/PM values will all have to be flipped at the end.

The other critical assumption that I am making for this algorithm is that the times are listed in a logical, ascending order. That is, if "10:00AM" is followed by "11:55", I am going to assume that "11:55" is also an "AM" listing. The only exception to this is if "11:55" is explicitly listed as a "PM" listing. In that case, I have put in some checks to deal with these known outliers.

Ok, let's take a look at the code:

  • <cffunction
  • name="otherTT"
  • access="public"
  • returntype="string"
  • output="false"
  • hint="Given an AM/PM value, I return the opposite AM/PM value.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="tt"
  • type="string"
  • required="true"
  • hint="I am either AM or PM."
  • />
  •  
  • <!--- Check to see which value we currently have. --->
  • <cfif (arguments.tt eq "AM")>
  •  
  • <!--- Return opposite. --->
  • <cfreturn "PM" />
  •  
  • <cfelse>
  •  
  • <!--- Return opposite. --->
  • <cfreturn "AM" />
  •  
  • </cfif>
  • </cffunction>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!--- Define our list of raw showtimes. --->
  • <cfset showtimesList = "11:00 11:15AM 1:15 1:35 1:50 4:10 4:25 6:30 6:50 8:50 9:20 10:50 12:00 12:30 12:45AM" />
  •  
  •  
  • <!--- Define our collection of fleshed-out showtimes. --->
  • <cfset showtimes = [] />
  •  
  •  
  • <!---
  • We are going to run through the movie showtimes with an
  • initial assumption that they start in the AM. If we come across
  • any showtimes that contradict this, we'll go back through the
  • showtimes and change the AM/PM markers.
  • --->
  • <cfset flipAMPM = false />
  •  
  • <!---
  • We will start out by assuming that the first showtime will be
  • in the AM. But, it doesn't really matter.
  • --->
  • <cfset prevTT = "AM" />
  •  
  • <!--- Keep track of the prevous time. --->
  • <cfset prevHHMM = "" />
  •  
  •  
  • <!---
  • Keep a collection of known AM/PM values. This will help us deal
  • with very outlier cases in which movie showtimes are spread
  • extermely far apart.
  • --->
  • <cfset knownTT = {} />
  •  
  •  
  • <!---
  • Loop over all the raw showtimes so that we can convert each one
  • into a showtime containing both a HH:MM and a TT portion.
  • --->
  • <cfloop
  • index="showtimeItem"
  • list="#showtimesList#"
  • delimiters=" ">
  •  
  •  
  • <!--- Extract the HH:MM and the TT. --->
  • <cfset showtimeParts = reMatchNoCase(
  • "\d+:\d+|AM|PM",
  • showtimeItem
  • ) />
  •  
  • <!---
  • Append the empty string just to simplify our logic. This way,
  • the parts array will always have at least two indices.
  • --->
  • <cfset arrayAppend( showtimeParts, "" ) />
  •  
  •  
  • <!--- Get the time-only value (without any AM/PM). --->
  • <cfset thisHHMM = showtimeParts[ 1 ] />
  •  
  • <!---
  • Get the AM/PM part. If this was not provided in the raw
  • showtime, then this will be an empty string.
  • --->
  • <cfset rawTT = showtimeParts[ 2 ] />
  •  
  •  
  • <!--- Check to see if this is the first item. --->
  • <cfif !arrayLen( showtimes )>
  •  
  •  
  • <!---
  • Check to see if a raw TT was provided. If so, then we
  • just want to use it since it is the only "sure" thing
  • we know at this point.
  • --->
  • <cfif len( rawTT )>
  •  
  • <!--- Use the given TT. --->
  • <cfset thisTT = rawTT />
  •  
  • <cfelse>
  •  
  • <!--- Assume that it was AM. --->
  • <cfset thisTT = prevTT />
  •  
  • </cfif>
  •  
  •  
  • <!--- Construct the first time. --->
  • <cfset thisTime = (thisHHMM & thisTT) />
  •  
  •  
  • <cfelse>
  •  
  •  
  • <!---
  • At first, we will assume that this time as the same
  • AM/PM as the previous time. However, if we are actually
  • crossing over the divide, then this time will SMALLER
  • than the previous time.
  • --->
  • <cfif ("#prevHHMM##prevTT#" lte "#thisHHMM##thisTT#")>
  •  
  • <!---
  • The times still appear to be asscending. This
  • indicates that we have not jumped over any divide in
  • the day.
  • --->
  • <cfset thisTT = prevTT />
  •  
  • <cfelse>
  •  
  • <!---
  • We have jumped over a divide in the day (either going
  • from AM to PM or PM to AM). Get the current AM/PM by
  • swapping the previous one.
  • --->
  • <cfset thisTT = otherTT( prevTT ) />
  •  
  • </cfif>
  •  
  • <!--- Construct the showtime. --->
  • <cfset thisTime = (thisHHMM & thisTT) />
  •  
  • <!---
  • Check to see if the actual AM/PM value was provided in
  • the raw data. If so, we can compare this known value to
  • our assumed value.
  • --->
  • <cfif (
  • len( rawTT ) &&
  • (rawTT neq thisTT)
  • )>
  •  
  • <!---
  • Our first assumption was wrong. When we are done,
  • we will have to swap all of our AM/PM values.
  • --->
  • <cfset flipAMPM = true />
  •  
  • </cfif>
  •  
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Check to see if we have a rawTT - that is, one of the few
  • known AM/PM values. If so, we want to make sure that don't
  • ever change this one.
  •  
  • NOTE: This will only come into play if we need to "flip" the
  • AM/PM values.
  • --->
  • <cfif len( rawTT )>
  •  
  • <!--- Store the known AM/PM value. --->
  • <cfset knownTT[ arrayLen( showtimes ) + 1 ] = rawTT />
  •  
  • </cfif>
  •  
  •  
  • <!--- Store the previous time properties. --->
  • <cfset prevTT = thisTT />
  • <cfset prevHHMM = thisHHMM />
  •  
  • <!--- Add the stored time. --->
  • <cfset arrayAppend( showtimes, thisTime ) />
  •  
  •  
  • </cfloop>
  •  
  •  
  • <!--- Check to see if we need to flip the AM/PM values. --->
  • <cfif flipAMPM>
  •  
  • <!--- Loop over each value to swap it. --->
  • <cfloop
  • index="index"
  • from="1"
  • to="#arrayLen( showtimes )#"
  • step="1">
  •  
  • <!--- Get the current time parts. --->
  • <cfset showtimeParts = reMatchNoCase(
  • "\d+:\d+|AM|PM",
  • showtimes[ index ]
  • ) />
  •  
  • <!---
  • Check to see if this index has a known AM/PM value.
  • If that is the case, then we don't want to use our own
  • logic - we just want to use the original value.
  • --->
  • <cfif structKeyExists( knownTT, index )>
  •  
  • <!---
  • This is a known value. Use it instead of any AM/PM
  • swapping techniques.
  • --->
  • <cfset showtimes[ index ] = (
  • showtimeParts[ 1 ] &
  • knownTT[ index ]
  • ) />
  •  
  • <cfelse>
  •  
  • <!---
  • This is not a known value - simply reconstruct the
  • new time with the swapped AM/PM.
  • --->
  • <cfset showtimes[ index ] = (
  • showtimeParts[ 1 ] &
  • otherTT( showtimeParts[ 2 ] )
  • ) />
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  • </cfif>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!--- Output the original showtimes and our new showtimes. --->
  • <cfoutput>
  •  
  • <strong>Original</strong>:
  •  
  • #lcase( showtimesList )#
  •  
  • <br />
  • <br />
  •  
  • <strong>Parsed</strong>:
  •  
  • #lcase( arrayToList( showtimes, " " ) )#
  •  
  • </cfoutput>

When we run the above code, we get the following output:

Original: 11:00 11:15am 1:15 1:35 1:50 4:10 4:25 6:30 6:50 8:50 9:20 10:50 12:00 12:30 12:45am

Parsed: 11:00am 11:15am 1:15pm 1:35pm 1:50pm 4:10pm 4:25pm 6:30pm 6:50pm 8:50pm 9:20pm 10:50pm 12:00am 12:30am 12:45am

As you can see, the AM/PM values have been added to all items that were previously missing them, thereby creating a normalized showtime list.

This seems like an overly complicated algorithm; but, honestly, this was as simple as I could possibly make it. If you can come up with a way to simplify this even more, I would love to see it! Like I said, this was the best I could come up with after many failed attempts.




Reader Comments

I will admit I haven't given this a lot of thought, but I think a different way of approaching this is to first scan the list for the first known AM/PM value. Once you find one, mark that value and go backwards to the front of the list and calculate what the AM/PM value should be and apply it. Then go back to the time you marked and go forward through the list applying AM/PM in the same fashion.

Reply to this Comment

@TJ,

Thanks - I keep thinking there has *got* to be an easier solution to this. But, I cannot for the life of me figure it out.

I guess leaving out AM/PM makes the showtimes easier to read? I mean, most people go to see movies between like 5PM and 11PM. I guess Google just figures they don't need it.

Or maybe, leaving out 2-characters here and there actually saves Google like Terabytes of data transfer :D I am sure they get some retarded huge number of page views!

Reply to this Comment

@Aaron,

That was actually the approach that I tried *right* before I came up with this one. I didn't bring that approach to completion however as I had the idea for this approach while coding that approach. In the end, though, I don't think you save much by finding the first known AM/PM value since there are multiple cross-overs if the showtimes start early (11:AM) and end very late the next morning (12:30AM).

Feel free to have a go at it - I'm open to all suggestions.

Reply to this Comment

@Andy,

I wanted to see if I could "normalize" the data so that I maybe able to more effectively use it (programmatically) one day.

Reply to this Comment

Yeah, with something like this, you don't really think it's going to be that complex until you roll up your sleeves, sit down, and actually start coding the logic. For one of my projects, I actually had a component I was writing for a website where we posted movie times/theatres, etc. I went back to see if we had published am/pm movie times for those, because I didn't remember, but I guess that particular component has been taken off the website or moved to somewhere pretty much impossible to find. I also worked at one time for a television station. We posted showtimes for programs that were on that station throughout the day. While that in itself was not a huge deal, we also had these "program exceptions"...for example, a President's speech would come on during a time when a regularly scheduled program was supposed to be on. Now THAT became complex. Trust me, it's much more complex than it sounds. Or, at least it was to me, especially being that it was my very first ColdFusion job ever, so I was really new to ColdFusion at that point. Anyway, thanks for the memories! :-) I love it when I read posts like this and I am brought back to fond memories of previous projects. :-)

Reply to this Comment

I am looking at the google showtime results and there does seem to be some logic to it (granted I only went through a couple days examples but they all seem to meet this criteria). It seems the AM/PM shows up on the *last* item of the list. So if you had the following:

10:00 11:15am 1:00 4:00 9:00pm

The last item before a change happens is the one indicating the AM/PM. You could probably just parse the list in reverse and then assume that if it does not contain an AM or PM it is the same as the prior one (which is of course really the following one since you are working in reverse).

Like I said, maybe the amount of data I looked through wasn't fully representative but it would surprise the heck out of me if there were no logical reasoning behind where they place this value.

Reply to this Comment

@Brian,

Dang! You might just be right! That's why you get paid the big bucks :) I think what threw me were the parts of the list were the was only one item. In that case, the item is *both* the first and the last item.

That might actually make things easier. If you find a "known" instance, you can then fill in anything unknown *before* it with the same TT value. You can then do this going forward.

I'm not saying that code would be any easier... but the concept might be :)

@Anna,

Word up - some concepts are deceptively hard. Although, I suppose someone can always come along and make them much easier :)

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.