--[[----------------------------------------------------------------------------

KSDate.lua

--------------------------------------------------------------------------------

ADOBE CONFIDENTIAL
------------------
Copyright 2011 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remain the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

------------------------------------------------------------------------------]]

	-- core.os
local KSDateImp = require 'KSDateImp' -- intended only for use by this namespace

	-- WF.core.utils
local WFArguments = require 'WFArguments'

	-- WF.core.utils
local WFPlatformInfo = require 'WFPlatformInfo'

--============================================================================--
--[[!

This namespace provides cross-platform utilities for manipulating date-time values in Lua.

]]--

--@mark class KSDate

local KSDate = {}

--------------------------------------------------------------------------------
--[[!
	Retrieves the current date-time as the number of seconds from the reference date (2001-01-01)
	
	\param optUseUnixEpoch If supplied returns the number of seconds from the unix epoch (1970-01-01) 
		instead of the reference date

	\return A number of seconds.
]]--

KSDate.timeNow = KSDateImp.timeNow

--[[!
	Retrieves the current time in terms of clock ticks.

	\return A number of clock ticks.
]]--

KSDate.timeNowForAnimation = KSDateImp.timeNowForAnimation

--[[!
	Retrieves the time since the parent application was launched.

	\return A number of seconds.
]]--

KSDate.timeIntervalSinceAppLaunch = KSDateImp.timeIntervalSinceAppLaunch

--[[!
	Retrieves the current date-time and reports it as an ISO 8601
	formatted string with components normalized to the time zone
	of device's local calendar setting.

	\return  An ISO 8601 Date string.
]]--

KSDate.currentDateTimeInLocalizedComponents = KSDateImp.currentDateTimeInLocalizedComponents

local decomposeTime = assert( KSDateImp.decomposeTime )

local KSDateImp_politeDate = assert( KSDateImp.politeDate )

--------------------------------------------------------------------------------

--[[!
     Retrieves the current or a specified date-time as a string with formatting options.

     	\param optTime If supplied, the date-time value to format. If nil, retrieves
			and formats the current time.
	\param optUseLocalTimeZone When true, uses the time zone of device's local calendar setting.
			If nil or false, uses GMT.
	\param formatString A Lua format string.

	\return The formatted date-time string.
]]--

local function getTime( optTime, optUseLocalTimeZone, optUseUnixEpoch, formatString )

	-- Break time down into its components. If optTime is nil, we'll use current time.
	-- if optUseLocalTimeZone is true, we'll use the timezone set by the system, otherwise GMT.
	
	if not optTime then
		optTime = KSDateImp.timeNow()
	end
	
	-- round up to the nearest millisecond
	optTime = math.floor( optTime * 1000 + 0.5 ) / 1000
	
	local year, month, day, hour, minute, second = decomposeTime( optTime, optUseLocalTimeZone, optUseUnixEpoch )
	
	local time = string.format( formatString, year, month, day, hour, minute, second )
	
	-- Add the "Z" designator for UTC
	if not optUseLocalTimeZone then
		time = time .. "Z"
	end
	
	return time

end

--------------------------------------------------------------------------------

--[[!
     Retrieves the current or a specified date-time as multiple return values:
	 
	 local year, month, day, hour, minute, second =  KSDate.getDecomposedTime()

    \param optTime If supplied, the date-time value to format. If nil, retrieves
			and formats the current time.
	\param optUseLocalTimeZone When true, uses the time zone of device's local calendar setting.
			If nil or false, uses GMT.
 
	\return year, month, day, hour, minute, second
]]--

function KSDate.getDecomposedTime( optTime, optUseLocalTimeZone )

	-- Break time down into its components. If optTime is nil, we'll use current time.
	-- if optUseLocalTimeZone is true, we'll use the timezone set by the system, otherwise GMT.
	
	if not optTime then
		optTime = KSDateImp.timeNow()
	end
	
	-- round up to the nearest millisecond
	optTime = math.floor( optTime * 1000 + 0.5 ) / 1000
	
	return decomposeTime( optTime, optUseLocalTimeZone )

end

--------------------------------------------------------------------------------

--[[!
     Retrieves the current or a specified date-time as an ISO 8601 Date string.

     	\param optTime If supplied, the date-time value to format. If nil, retrieves
			and formats the current time.
	\param optUseLocalTimeZone When true, uses the time zone of device's local calendar setting.
			If nil or false, uses GMT.
 
	\return The formatted date-time string.
]]--

function KSDate.timeInIso8601Format( optTime, optUseLocalTimeZone )

	return getTime( optTime, optUseLocalTimeZone, nil, "%04d-%02d-%02dT%02d:%02d:%06.3f" )

end

--------------------------------------------------------------------------------

--[[!
     Retrieves the current or a specified date-time as an ISO 8601 Date string, rounding
     to the nearest whole second.

     \param optTime If supplied, the date-time value to format. If nil, retrieves
			and formats the current time.
	\param optUseLocalTimeZone When true, uses the time zone of device's local calendar setting.
			If nil or false, uses GMT.
 
	\return The formatted date-time string.

]]--

function KSDate.timeInIso8601FormatNearestSecond( optTime, optUseLocalTimeZone )

	return getTime( optTime, optUseLocalTimeZone, nil, "%04d-%02d-%02dT%02d:%02d:%02d" )

end

--------------------------------------------------------------------------------

--[[!
     Retrieves a specified date-time as an ISO 8601 Date string.

    \param UnixEpochTime Unix epoch time interval in seconds since 00:00:00 UTC 1st January 1970
	\param optUseLocalTimeZone When true, uses the time zone of device's local calendar setting.
			If nil or false, uses GMT.
 
	\return The formatted date-time string.
]]--

function KSDate.timeInIso8601FormatWithUnixEpochInterval( UnixEpochTime, optUseLocalTimeZone )

	 WFArguments.check( 'number', '?boolean' )

	local optUseUnixEpoch = true
	return getTime( UnixEpochTime, optUseLocalTimeZone, optUseUnixEpoch, "%04d-%02d-%02dT%02d:%02d:%06.3f" )

end


--------------------------------------------------------------------------------

--[[!
     Retrieves a specified date-time .
	 
	\return Unit epoch time interval in seconds since 00:00:00 UTC 1st January 1970
]]--

function KSDate.timeIntervalSinceUnixEpoch()

	local time = KSDateImp.timeNow( true )
	
	-- round up to the nearest millisecond
	time = math.floor( time * 1000 + 0.5 ) / 1000
	
	return time

end

--------------------------------------------------------------------------------

--[[!
     Retrieves the current or a specified date-time as an ISO 8601 Date string, rounding
     to the nearest whole day.

     	\param optTime If supplied, the date-time value to format. If nil, retrieves
			and formats the current time.
	\param optUseLocalTimeZone When true, uses the time zone of device's local calendar setting.
			If nil or false, uses GMT.
 
	\return The formatted date-time string.

]]--

function KSDate.dateInIso8601Format( optTime, optUseLocalTimeZone )

	return getTime( optTime, optUseLocalTimeZone, nil, "%04d-%02d-%02d" )

end

--------------------------------------------------------------------------------

--[[!
     Retrieves the current time zone of the device's local calendar setting.
  
	\return The time zone in ISO 8601 format.

]]--

function KSDate.timeZoneInIso8601Format()

	return KSDateImp.timeZoneInIso8601Format()

end

--------------------------------------------------------------------------------

--[[!
     Retrieves the current or a specified date-time as an ISO 8601 Date string,
     with a time zone component from the device's local calendar setting.

     	\param optTime If supplied, the date-time value to format. If nil, retrieves
			and formats the current time.
  
	\return The formatted date-time string.
]]--

function KSDate.timeInIso8601FormatWithTimeZone( optTime )

	return KSDate.timeInIso8601Format( optTime, true )
		.. KSDate.timeZoneInIso8601Format()
	
end

--------------------------------------------------------------------------------
--[[!
   Truncates an IS08601 date-time string, returning the first 19 characters and discarding
   any subsecond time and timezone information.

   \param dateTime The IS08601 date-time string.
   
   \return An IS08601 date-time string in the format 'YYYY-MM-DDTHH:mm:ss'.
]]--

function KSDate.splitIso8601DateUptoSeconds( dateTime )

	return dateTime:sub( 1, 19 ), string.sub( dateTime, 20 )

end

--------------------------------------------------------------------------------

local politeDateCacheSize = 100

-- Caching for polite dates based on the whether the value is a date-time value
-- or the formatting style specified.
-- Dates that don't fall into the above categories are cached in the default cache

local politeDateCache  = {}

local cachedPoliteDateKeys = {
	default = true,
	medium = true,
	long = true,
}

local KSMemoryCache -- lazy loaded


--------------------------------------------------------------------------------
 
--[[!
	Creates an ISO 8601 date string from component values and caches the new
	string.

	\param y The numeric year component.
	\param m The numeric month component.
	\param d The numeric day component.
	\param optStyle	The format style, 'medium' (includes a time with no seconds component)
			or 'long'.

	\return The new string formatted in the given style.

]]--

function KSDate.politeDate( y, m, d, optStyle )

	local key = string.format( '%s-%s-%s', y, m, d )
	
	local cacheType = 'default'

	if optStyle then
		cacheType = optStyle
	end
	
	if not KSMemoryCache then
		KSMemoryCache = assert( require 'KSMemoryCache' )
	end
	
	if cachedPoliteDateKeys[ optStyle ] and not politeDateCache[ cacheType ] then
		politeDateCache[ cacheType ] = KSMemoryCache( politeDateCacheSize )
	end
	
	-- If this date is cached, return the cached polite date.

	local result = politeDateCache[ cacheType ] and politeDateCache[ cacheType ]:getData( key )
	
	if result then
		return result
	end

	-- If not, get the formatted date and write it to cache.

	result = KSDateImp_politeDate( y, m, d, nil, nil, nil, optStyle )
	
	if politeDateCache[ cacheType ] then
		politeDateCache[ cacheType ]:add( key, result )
	end
	
	return result

end

--------------------------------------------------------------------------------
 
--[[!
	Formats the date-time value to the formatted date string.
	Does not cache the formatted values (as is done by politeDate)

	\param timestamp The numeric timestamp value.
	\param optDateStyle The format style for the date component.
	\param optTimeStyle The format style for the time component.

	\return The formatted string.

]]--

function KSDate.politeDateFromTime( time, optDateStyle, optTimeStyle )

	local dateString = KSDate.timeInIso8601Format( time )

	return KSDate.politeDateFromISO8601DateString( dateString, optDateStyle, optTimeStyle )

end

--------------------------------------------------------------------------------
 
--[[!
	Formats a ISO86 date string to the formatted date string, consider the current calendar
	Does not cache the formatted values (as is done by politeDate)

	\param dateString The ISO86 date string.
	\param optDateStyle The format style for the date component.
	\param optTimeStyle The format style for the time component.

	\return The formatted string.

]]--

function KSDate.politeDateFromISO8601DateString( dateString, optDateStyle, optTimeStyle )
	
	local format = "(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)"

	local y, m, d, hh, mm, ss = dateString:match( format )
	
	-- Special case for the zero-date that Oz expects
	if tonumber( y ) == 0 and
		tonumber( m ) == 0 and
		tonumber( d ) == 0 and
		tonumber( hh ) == 0 and
		tonumber( mm ) == 0 and
		tonumber( ss ) == 0 then
		return nil
	end
	
	return KSDateImp_politeDate( y, m, d, hh, mm, ss, optDateStyle, optTimeStyle )

end

--------------------------------------------------------------------------------

--[[!
	Formats a ISO86 date string to the long formatted date string (strip day of the week)
	
	consider the current local calendar

	\param dateString The ISO86 date string.
	\param optDateStyle The format style for the date component.
	\param optTimeStyle The format style for the time component.

	\return The formatted string.

]]--
 
function KSDate.formatDateTimeFromISO8601DateString( dateString, optDateStyle, optTimeStyle )
	
	local format = "(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)"

	local y, m, d, hh, mm, ss = dateString:match( format )
	
	-- Special case for the zero-date that Oz expects
	if tonumber( y ) == 0 and
		tonumber( m ) == 0 and
		tonumber( d ) == 0 and
		tonumber( hh ) == 0 and
		tonumber( mm ) == 0 and
		tonumber( ss ) == 0 then
		return nil
	end
		
	if WFPlatformInfo.isWindows then
	
		--OptDateStyle:
			-- Full: use "long date" format in the region & language settings
			-- Long: strip the day of week
			-- Medium: strip the day of week, and use abbreviated month
			-- Short: use "short date" format.
		--OptTimeStyle
			-- Long: use default time style
			-- Medium: use default time style
			-- Short: do not use second
			-- none: do not use time
	
		return KSDateImp.formatDateTime( y, m, d, hh, mm, ss, optDateStyle, optTimeStyle )
	else
		return KSDateImp_politeDate( y, m, d, hh, mm, ss, optDateStyle, optTimeStyle )
	end
	
end

--------------------------------------------------------------------------------

--[[!
    Get the date component format string

	\params
		\param dateString The ISO86 date string.
		\param dateComponent month, or ampm.
		\param useAbbreviatedText.
		
    \return the date comonent format string
]]--

function KSDate.formatDateComponentFromISO8601DateString( dateString, dateComponent, useAbbreviatedText )
	
	local format = "(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)"

	local y, m, d, hh, mm, ss = dateString:match( format )
	
	-- Special case for the zero-date that Oz expects
	if tonumber( y ) == 0 and
		tonumber( m ) == 0 and
		tonumber( d ) == 0 and
		tonumber( hh ) == 0 and
		tonumber( mm ) == 0 and
		tonumber( ss ) == 0 then
		return nil
	end

	return KSDateImp.formatDateComponent( y, m, d, hh, mm, ss, dateComponent, useAbbreviatedText )
end

--------------------------------------------------------------------------------
 
--[[!
     Reports the difference between two dates in days, ignoring the time components.

       \param startDate The first date-time value, an ISO 8601 date string.
       \param endDate The second date-time value, an ISO 8601 date string.
 
       \return A number of days, negative if the first day is later than the second day.


]]--

function KSDate.differenceInDays( startDate, endDate )

	return KSDateImp.diffInDays( startDate, endDate )
	
end

--------------------------------------------------------------------------------
 
--[[!
     Reports the difference between two date-times in seconds.

       \param startDate The first date-time value, an ISO 8601 date string.
       \param endDate The second date-time value, an ISO 8601 date string.

       \return A number of seconds, negative if the first time is later than the second time.

]]--

function KSDate.differenceInSeconds( startDate, endDate )

	return KSDateImp.diffInSeconds( startDate, endDate )

end

--------------------------------------------------------------------------------

--[[!
	Reports the difference between two date-times, for each component
	(year, month, day, hour, minute, second). For example, if the
	two dates differ by 3 seconds, all other components have a difference
	of 0. Differences are computed relative to the device's local calendar
	setting.

       \param startDate The first date-time value, an ISO 8601 date string.
       \param endDate The second date-time value, an ISO 8601 date string.


	\return	 First return value is true if the difference is negative,
		meaning that the first date is later than the second, false otherwise.
		The next six return values are the differences in each date component
		in order: year, month, day, hour, minute, second.

]]--

function KSDate.localizedDiffInAllComponents( startDate, endDate )
	
	return KSDateImp.localizedDiffInAllComponents( startDate, endDate )
	
end

--------------------------------------------------------------------------------

--[[!
    Formats a numeric date-time value in the local timezone into ISO8601 format.

    \param date The numeric date-time value.

    \return The formatted date-time.
]]--

function KSDate.formatDateTimeAsLocal( date )
	
	return KSDateImp.formatDateTimeAsLocal( date )
	
end

--------------------------------------------------------------------------------

--[[!
    Formats a numeric date-time value in the UTC timezone into ISO8601 format.

    \param date The numeric date-time value.

    \return The formatted date-time.
]]--

function KSDate.formatDateTimeAsUTC( date )
	
	return KSDateImp.formatDateTimeAsUTC( date )
	
end

--------------------------------------------------------------------------------

--[[!
    Converts an ISO8601 date string to the numeric date-time value

    \param date ISO8601 formatted date string. This may look like 
		2011-02-17T18:15:04.54388Z, 2011-02-17T18:15:04.54388-0600, 
		or 2011-02-17T18:15:04.54388

    \return The numeric date-time value.
]]--

function KSDate.iso8601ToTimeIntervalSinceReference( date )
	
	return KSDateImp.iso8601ToTimeIntervalSinceReference( date )

end

--------------------------------------------------------------------------------

--[[!
    Returns the number of seconds difference between the local time zone and
	GMT

    \return The number of seconds between the local machine and GMT and a
			boolean value indicating whether it is daylight savings time
]]--

function KSDate.timeZoneAdjustmentInSeconds( )
	
	return KSDateImp.timeZoneAdjustmentInSeconds( )

end

--------------------------------------------------------------------------------

local kDatePatterns = {
	"^(%d%d%d%d)-(%d+)-(%d+)T(%d+):(%d+):([0-9.]+)([-+]%d+:%d+)%s*$",				-- YYYY-MM-DDTHH:MM:SS[.SS]+/-HH:MM
	"^(%d%d%d%d)-(%d+)-(%d+)T(%d+):(%d+):([0-9.]+)([-+][0-9][0-9][0-9]-)%s*$",		-- YYYY-MM-DDTHH:MM:SS[.SS]+/-HHMM
	"^(%d%d%d%d)-(%d+)-(%d+)T(%d+):(%d+):([0-9.]+)(Z)%s*$",							-- YYYY-MM-DDTHH:MM:SS[.SS]Z
	"^(%d%d%d%d)-(%d+)-(%d+)T(%d+):(%d+):([0-9.]+)%s*$",							-- YYYY-MM-DDTHH:MM:SS[.SS]
	
	"^(%d%d%d%d)-(%d+)-(%d+)T(%d+):(%d+)()([-+]%d+:%d+)%s*$",						-- YYYY-MM-DDTHH:MM+/-HH:MM
	"^(%d%d%d%d)-(%d+)-(%d+)T(%d+):(%d+)()([-+][0-9][0-9][0-9]-)%s*$",				-- YYYY-MM-DDTHH:MM+/-HHMM
	"^(%d%d%d%d)-(%d+)-(%d+)T(%d+):(%d+)()(Z)%s*$",									-- YYYY-MM-DDTHH:MMZ
	"^(%d%d%d%d)-(%d+)-(%d+)T(%d+):(%d+)%s*$",										-- YYYY-MM-DDTHH:MM
	
	"^(%d%d%d%d)-(%d+)-(%d+)T(%d+)()()([-+]%d+:%d+)%s*$",								-- YYYY-MM-DDTHH+/-HH:MM
	"^(%d%d%d%d)-(%d+)-(%d+)T(%d+)()()([-+][0-9][0-9][0-9]-)%s*$",					-- YYYY-MM-DDTHH+/-HHMM
	"^(%d%d%d%d)-(%d+)-(%d+)T(%d+)()()(Z)%s*$",										-- YYYY-MM-DDTHHZ
	"^(%d%d%d%d)-(%d+)-(%d+)T(%d+)%s*$",											-- YYYY-MM-DDTHH

	"^(%d%d%d%d)-(%d+)-(%d+)%s*$",													-- YYYY-MM-DD
	"^(%d%d%d%d)-(%d+)%s*$",														-- YYYY-MM
	"^(%d%d%d%d)%s*$",																-- YYYY
}

local kDefaultDateComponents = {
    '2001', '01', '01', '00', '00', '00', ''
--	 YYYY    MM    DD    HH    MM    SS   TZD
}


--[[!
    Try to create a KSDate-compatible string

    \return A KSDate string if possible, filling in the missing fields. Nil otherwise.
]]--

function KSDate.maybeKSDateFromString( possibleDate )

	if type( possibleDate ) == 'string' then
		for _, pattern in ipairs( kDatePatterns ) do
			
			local components = { string.match( possibleDate, pattern ) }
			
			if components[ 1 ] then
				for i = 2, #kDefaultDateComponents do
					if type( components[ i ] ) ~= 'string' then
						components[ i ] = kDefaultDateComponents[ i ]
					end
				end
				
				local assembledDate = 
					components[ 1 ] .. '-' ..			-- YYYY
					components[ 2 ] .. '-' ..			-- MM
					components[ 3 ] .. 'T' ..			-- DD
					components[ 4 ] .. ':' ..			-- HH
					components[ 5 ] .. ':' ..			-- MM
					components[ 6 ] ..					-- SS
					components[ 7 ]						-- TZD
					
				return assembledDate
			end
		end
	end
	
	-- No match found. Fall back to the simple definition of KSDate
	return KSDate.isKSDate( possibleDate )
end

--------------------------------------------------------------------------------

--[[!
    Validate if an argument is a valid KSDate

    \return True if valid possibleDate KSDate, otherwise false.
]]--

function KSDate.isKSDate( possibleDate )
	
	return type(possibleDate) == 'string'
			and possibleDate:match( '^%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d:%d%d' )

end

--------------------------------------------------------------------------------

--[[!
    Try to recover a string formatted date.

    \return a valid date if one could be recovered from possibleDate, otherwise nil.
]]--

function KSDate.recoverDateForOz( possibleDate )

	if type( possibleDate ) ~= 'string' then
		return nil
	end

	for _, pattern in ipairs( kDatePatterns ) do
		
		local components = { string.match( possibleDate, pattern ) }
		
		if components[ 1 ] then
			for i = 2, #kDefaultDateComponents do
				if type( components[ i ] ) ~= 'string' then
					components[ i ] = kDefaultDateComponents[ i ]
				end
			end
			
			local ss, ms = string.match( components[ 6 ], '([0-9]+)\.([0-9]+)')
			if not ss then
				ss = string.match( components[ 6 ], '([0-9]+)')
			end
			
			-- fix dates with 60 seconds, blissfully ignoring leap seconds
			local seconds = tonumber( ss )
			-- we only deal with seconds = 60, not anything higher
			if seconds == 60 then
			
				components[ 6 ] = '59'
				if ms then
					-- limit to 1/10th of a second, because iso8601ToTimeIntervalSinceReference
					-- accepts only 1 digit for ms
					components[ 6 ] = components[ 6 ] .. '.' .. string.sub( ms, 1, 1 )
				end
			
			end
			
			local fixedDate = 
				components[ 1 ] .. '-' ..			-- YYYY
				components[ 2 ] .. '-' ..			-- MM
				components[ 3 ] .. 'T' ..			-- DD
				components[ 4 ] .. ':' ..			-- HH
				components[ 5 ] .. ':' ..			-- MM
				components[ 6 ] ..					-- SS
				components[ 7 ]						-- TZD
	
			local success, d = pcall( KSDate.iso8601ToTimeIntervalSinceReference, fixedDate )
			if success then
				return KSDate.timeInIso8601Format( d, false )
			end

			return nil
		end
	end
	

end

--------------------------------------------------------------------------------

--[[!
    Registers an argument checker to validate an argument is a date.
]]--

local dateCheckerRegistered = false

function KSDate.registerArgumentChecker()

	if not dateCheckerRegistered then

		local WFArguments = require 'WFArguments'

		WFArguments.registerChecker( 'KSDate', function( possibleDate )
			return KSDate.isKSDate( possibleDate )
		end)

	end

	dateCheckerRegistered = true

end

--------------------------------------------------------------------------------

--[[!
    Create a data components array for a giving date string

    \return data components array, otherwise nil
]]--

function KSDate.maybeDateComponents( possibleDate )
	local dateComponents = nil

	if type ( possibleDate ) == 'string' then
		
		for _, pattern in ipairs( kDatePatterns ) do
			
			local components = { string.match( possibleDate, pattern ) }
			
			if components[ 1 ] then
				for i = 2, #kDefaultDateComponents do
					if type( components[ i ] ) ~= 'string' then
						components[ i ] = kDefaultDateComponents[ i ]
					end
				end
				
				dateComponents = {}
				dateComponents.year = math.floor( components[ 1 ] )
				dateComponents.month = math.floor( components[ 2 ] )
				dateComponents.day = math.floor( components[ 3 ] )
				dateComponents.hour = math.floor( components[ 4 ] )
				dateComponents.minute = math.floor( components[ 5 ] )
				dateComponents.second = math.floor( components[ 6 ] )
				dateComponents.tz = components[ 7 ]
				
				return dateComponents
			end
		end
	
	end
	
	return dateComponents
end

--------------------------------------------------------------------------------

--[[!
    Try to create a KSDate-compatible string

    \return A KSDate string if possible
]]--

function KSDate.maybeKSDateFromDateComponents( components )
	local dateStringArray = nil
	local dateString = nil
	
	if components == nil or type( components) ~= 'table' then
		return nil
	end
	
	dateStringArray = {}
	
	dateStringArray[ 1 ] = string.format( "%04d", components.year )
	dateStringArray[ 2 ] = '-'
	dateStringArray[ 3 ] = string.format( "%02d", components.month )
	dateStringArray[ 4 ] = '-'
	dateStringArray[ 5 ] = string.format( "%02d", components.day )
	dateStringArray[ 6 ] = 'T'
	dateStringArray[ 7 ] = string.format( "%02d", components.hour )
	dateStringArray[ 8 ] = ':'
	dateStringArray[ 9 ] = string.format( "%02d", components.minute )
	dateStringArray[ 10 ] = ':'
	dateStringArray[ 11 ] = string.format( "%02d", components.second )
	dateStringArray[ 12 ] = components.tz
	
	dateString = table.concat( dateStringArray, '' )

	if KSDate.isKSDate( dateString ) then
		return dateString
	end
	
	return nil
end


--------------------------------------------------------------------------------

--[[!
    Convert 24 hour format to 12 hour format

    \return hour and ampm
]]--

function KSDate.convert24HourTo12Hour( inHour )
	local hour = tonumber( inHour )
	local ampm = nil
	
	if hour == 0 then
		hour = 12
		ampm = "am"
	elseif hour < 12 then
		ampm = "am"
	elseif hour == 12 then
		ampm = "pm"
	elseif hour > 12 then
		hour = hour - 12
		ampm = "pm"
	end
	
	return hour, ampm
end

--------------------------------------------------------------------------------

--[[!
    Convert 12 hour format to 24 hour format

    \return hour
]]--

function KSDate.convert12HourTo24Hour( inHour, ampm )
	local hour = tonumber( inHour )
	
	if ampm == "am" then
		-- AM
		if hour == 12 then
			hour = 0
		end
	else
		-- PM
		if hour == 12 then
			hour = 0
		end
		
		hour = hour + 12
	end
	
	return hour
end

--------------------------------------------------------------------------------

--[[!
    Create and returns a date string set to a given number of seconds from the specified date.

	\params
		\dateString	:	The date
		\timeDelta	:	number of seconds to add to the date
		
    \return the new date string set to a given number of seconds from the specified date.
]]--

function KSDate.shiftIso8601DateTime( dateString, timeDelta )
	
	-- Strip the timezone
	local dateTimeStr, otherDatePart = KSDate.splitIso8601DateUptoSeconds( dateString ) 
	
	local dateStringAsUTC = dateTimeStr .. "Z"
	
	-- Convert the date string to time and apply the time delta
	local time = KSDate.iso8601ToTimeIntervalSinceReference( dateStringAsUTC )
	
	local shiftTime = time + timeDelta
	
	local shiftDateString = KSDate.timeInIso8601Format( shiftTime )
	
	-- Add the timezone back if original date has timezone info
	shiftDateString = KSDate.splitIso8601DateUptoSeconds( shiftDateString ) 
	
	if otherDatePart then
		shiftDateString = shiftDateString .. otherDatePart
	end

	return shiftDateString
end

--------------------------------------------------------------------------------


--[[!
    Get the current date time format string

	\params
		\param optDateStyle The format style for the date component.
		\param optTimeStyle The format style for the time component.
		
    \return the date time format string, such as 'YYYY-MM-DD HH:mm:ss'
]]--


function KSDate.dateFormatString( optDateStyle,optTimeStyle )
	return KSDateImp.dateFormatString( optDateStyle, optTimeStyle )
end


--------------------------------------------------------------------------------

return KSDate
