% Copyright (C) 2015-2019 Olivier Boudeville % % This file is part of the Ceylan-Myriad library. % % This library is free software: you can redistribute it and/or modify % it under the terms of the GNU Lesser General Public License or % the GNU General Public License, as they are published by the Free Software % Foundation, either version 3 of these Licenses, or (at your option) % any later version. % You can also redistribute it and/or modify it under the terms of the % Mozilla Public License, version 1.1 or later. % % This library is distributed in the hope that it will be useful, % but WITHOUT ANY WARRANTY; without even the implied warranty of % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the % GNU Lesser General Public License and the GNU General Public License % for more details. % % You should have received a copy of the GNU Lesser General Public % License, of the GNU General Public License and of the Mozilla Public License % along with this library. % If not, see and % . % % Author: Olivier Boudeville [olivier (dot) boudeville (at) esperide (dot) com] % Creation date: Friday, July 24, 2015 % Gathering of time management facilities. % % See time_utils_test.erl for the corresponding test. % -module(time_utils). % Implementation notes: % % Native support in Erlang for time-related operations is mostly located in the % calendar module. % % A typical date format of interest here is: "Friday, July 24, 2015". % % A month is a positive integer, a canonical month is in [1,12]. % Month management support: -export([ canonicalise_month/1, check_month_canonical/1, check_month_order/2, month_to_string/1, week_day_to_string/1 ]). % Date support: -export([ compare_dates/2, check_date_order/2, get_date_difference/2 ]). % As calendar:daynum/0 is not exported: % % (Monday is 1, Tuesday is 2, etc.) % -type day_index() :: 1..7. % User-friendly version of day_index/0: -type week_day() :: 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday' | 'Sunday'. % Calendar date; used to be less precise calendar:date(): -type date() :: { unit_utils:year(), unit_utils:canonical_month(), unit_utils:canonical_day() }. % Time in the day; used to be { hour(), minute(), second() } or calendar:time(): -type time() :: { unit_utils:canonical_hour(), unit_utils:canonical_minute(), unit_utils:canonical_second() }. -export_type([ day_index/0, week_day/0, date/0, time/0 ]). % Basics: -export([ get_textual_date/1 ]). % For rough, averaged conversions: -export([ years_to_seconds/1, months_to_seconds/1, weeks_to_seconds/1, days_to_seconds/1, hours_to_seconds/1 ]). % Time-related section. -export([ get_intertime_duration/2 ]). % Timestamp-related section. -export([ get_timestamp/0, get_textual_timestamp/0, get_textual_timestamp/1, get_textual_timestamp_for_path/0, get_textual_timestamp_for_path/1, timestamp_to_string/1, string_to_timestamp/1, get_duration/1, get_duration/2, get_duration_since/1, get_textual_duration/2, get_precise_timestamp/0, get_precise_duration/2, get_precise_duration_since/1, get_date_after/2 ]). % Used to be calendar:datetime(), now uses our types: -type timestamp() :: { date(), time() }. -type precise_timestamp() :: { unit_utils:megaseconds(), unit_utils:seconds(), unit_utils:microseconds() }. % Cannot find the definition of the built-in timeout() type: -type time_out() :: 'infinity' | unit_utils:milliseconds(). % Designates an integer number of seconds since or before Unix time epoch, which % is 1970-01-01 00:00 UTC. % -type posix_seconds() :: integer(). -export_type([ timestamp/0, precise_timestamp/0, time_out/0, posix_seconds/0 ]). % Returns a string corresponding to the specified date, like: "30/11/2009". -spec get_textual_date( date() ) -> string(). get_textual_date( { Year, Month, Day } ) -> io_lib:format( "~B/~B/~B", [ Day, Month, Year ] ). % Month section. % Canonicalises specified month. -spec canonicalise_month( unit_utils:month() ) -> unit_utils:canonical_month(). canonicalise_month( M ) when is_integer( M ) andalso M >= 0 -> % Positive guard useful, as -1 rem 12 = -1 (hence not in [0,11]). % In [1;12]: case M rem 12 of 0 -> 12; Other -> Other end. % Checks that specified month is a canonical one. -spec check_month_canonical( unit_utils:month() ) -> void(). check_month_canonical( Month ) when is_integer( Month ) andalso Month >= 1 andalso Month =< 12 -> ok; check_month_canonical( Month ) -> throw( { non_canonical_month, Month } ). % Ensures that the starting canonical month is strictly before the stopping one. -spec check_month_order( unit_utils:absolute_month(), unit_utils:absolute_month() ) -> void(). check_month_order( Start={ StartYear, StartMonth }, Stop= { StopYear, StopMonth } ) -> check_month_canonical( StartMonth ), check_month_canonical( StopMonth ), case ( StartYear < StopYear ) orelse ( StartYear =:= StopYear andalso StartMonth < StopMonth ) of true -> ok; _False -> throw( { wrong_month_order, Start, Stop } ) end. % Converts a month (an integer in [1,12] or a 12-multiple thereof, like 23) into % its common name. % -spec month_to_string( unit_utils:month() ) -> string(). month_to_string( _MonthIndex=1 ) -> "January"; month_to_string( _MonthIndex=2 ) -> "February"; month_to_string( _MonthIndex=3 ) -> "March"; month_to_string( _MonthIndex=4 ) -> "April"; month_to_string( _MonthIndex=5 ) -> "May"; month_to_string( _MonthIndex=6 ) -> "June"; month_to_string( _MonthIndex=7 ) -> "July"; month_to_string( _MonthIndex=8 ) -> "August"; month_to_string( _MonthIndex=9 ) -> "September"; month_to_string( _MonthIndex=10 ) -> "October"; month_to_string( _MonthIndex=11 ) -> "November"; month_to_string( _MonthIndex=12 ) -> "December"; month_to_string( MonthIndex ) -> month_to_string( canonicalise_month( MonthIndex ) ). % Returns the common name of a day (ex: "Tuesday") based on the specified date % or on the specified index in the week. % -spec week_day_to_string( date() | day_index() ) -> string(). week_day_to_string( Date={ _Y, _M, _D } ) -> Day = calendar:day_of_the_week( Date ), week_day_to_string( Day ) ; week_day_to_string( _DayIndex=1 ) -> "Monday"; week_day_to_string( _DayIndex=2 ) -> "Tuesday"; week_day_to_string( _DayIndex=3 ) -> "Wednesday"; week_day_to_string( _DayIndex=4 ) -> "Thursday"; week_day_to_string( _DayIndex=5 ) -> "Friday"; week_day_to_string( _DayIndex=6 ) -> "Saturday"; week_day_to_string( _DayIndex=7 ) -> "Sunday"; week_day_to_string( DayIndex ) -> week_day_to_string( ( DayIndex rem 7 ) + 1 ). % Date section. % Checks that specified date is a canonical one. -spec check_date_canonical( date() ) -> void(). check_date_canonical( _Date={ Year, Month, Day } ) when is_integer( Year ) andalso is_integer( Month ) andalso is_integer( Day ) andalso Month >= 1 andalso Month =< 12 andalso Day >= 1 andalso Day =< 31 -> ok; check_date_canonical( Date ) -> throw( { non_canonical_date, Date } ). % Compares the specified two dates: tells whether the first date is strictly % before, after or the same as the second one. % % Note: both dates are expected to be in canonical form (ex: not more 12 months % or 31 days in the specified date). % -spec compare_dates( date(), date() ) -> basic_utils:comparison_result(). compare_dates( FirstDate, SecondDate ) -> check_date_canonical( FirstDate ), check_date_canonical( SecondDate ), compare_helper( FirstDate, SecondDate ). compare_helper( _FirstDate={ Yf, _Mf, _Df }, _SecondDate={ Ys, _Ms, _Ds } ) when Yf < Ys -> lower; compare_helper( _FirstDate={ Yf, _Mf, _Df }, _SecondDate={ Ys, _Ms, _Ds } ) when Yf > Ys -> higher; % From here, Yf =:= Ys: compare_helper( _FirstDate={ _Y, Mf, _Df }, _SecondDate={ _Y, Ms, _Ds } ) when Ms < Mf -> lower; compare_helper( _FirstDate={ _Y, Mf, _Df }, _SecondDate={ _Y, Ms, _Ds } ) when Ms > Mf -> higher; % From here, Yf =:= Ys and Mf =:= Ms: compare_helper( _FirstDate={ _Y, _M, Df }, _SecondDate={ _Y, _M, Ds } ) when Df < Ds -> lower; compare_helper( _FirstDate={ _Y, _M, Df }, _SecondDate={ _Y, _M, Ds } ) when Df > Ds -> higher; % Df =:= Ds, equality: %compare_helper( _FirstDate={ _Y, _M, _D }, % _SecondDate={ _Y, _M, _D } ) -> compare_helper( _FirstDate, _SecondDate ) -> equal. % Ensures that the starting canonical date is strictly before the stopping one. % % Note: both dates are expected to be in canonical form (ex: not more than 12 % months or 31 days in the specified date). % -spec check_date_order( date(), date() ) -> void(). check_date_order( StartDate, StopDate ) -> case compare_dates( StartDate, StopDate ) of lower -> ok; % Equal or higher: _ -> throw( { wrong_date_order, StartDate, StopDate } ) end. % Returns the signed duration, in days, between the two specified dates. -spec get_date_difference( date(), date() ) -> unit_utils:days(). get_date_difference( FirstDate, SecondDate ) -> FirstDayCount = calendar:date_to_gregorian_days( FirstDate ), SecondDayCount = calendar:date_to_gregorian_days( SecondDate ), SecondDayCount - FirstDayCount. % Time conversion section, based only on rather approximated values (ex: the % number of days varies from a year to another, so any constant value cannot be % accurate). % Converts a duration in years into a duration in seconds, supposing a year has % 365 days and 6 hours (i.e. a quarter of one day, to account for leap years). % -spec years_to_seconds( unit_utils:years() ) -> unit_utils:float_seconds(). years_to_seconds( YearDuration ) -> % 365.25 days per year one average here: YearDuration * 365.25 * 24 * 3600. % Converts a duration in months into a duration in seconds, supposing a month is % 1/12 of an average year. % -spec months_to_seconds( unit_utils:months() ) -> unit_utils:float_seconds(). months_to_seconds( MonthDuration ) -> MonthDuration * 365.25 / 12 * 24 * 3600. % Converts a duration in weeks into a duration in seconds. -spec weeks_to_seconds( unit_utils:weeks() ) -> unit_utils:seconds(). weeks_to_seconds( WeekDuration ) -> WeekDuration * 7 * 24 * 3600. % Converts a duration in days into a duration in seconds. -spec days_to_seconds( unit_utils:days() ) -> unit_utils:seconds(). days_to_seconds( DayDuration ) -> DayDuration * 24 * 3600. % Converts a duration in hours into a duration in seconds. -spec hours_to_seconds( unit_utils:hours() ) -> unit_utils:seconds(). hours_to_seconds( HourDuration ) -> HourDuration * 3600. % Time section. % Returns the signed duration, in integer seconds, between the two specified % times. % % A positive duration will be returned iff the first specified time is before % the second one. % -spec get_intertime_duration( time(), time() ) -> unit_utils:seconds(). get_intertime_duration( { H1, M1, S1 }, { H2, M2, S2 } ) -> ( ( H2 - H1 ) * 60 + ( M2 - M1 ) ) * 60 + ( S2 - S1 ). % Timestamp section. % Timestamp-related functions. % Returns a timestamp tuple describing the current time. % % Ex: { {Year,Month,Day}, {Hour,Minute,Second} } = time_utils:get_timestamp() % may return '{ {2007,9,6}, {15,9,14} }'. % -spec get_timestamp() -> timestamp(). get_timestamp() -> % Was: { erlang:date(), erlang:time() }. % Better: erlang:localtime(). % Returns a string corresponding to the current timestamp, like: % "2009/9/1 11:46:53". % % Note that the display order here is YY-MM-DD (same as when specifying the % timestamp), as opposed to DD-MM-YY, which is maybe more usual. % -spec get_textual_timestamp() -> string(). get_textual_timestamp() -> get_textual_timestamp( get_timestamp() ). % Returns a string corresponding to the specified timestamp, like: % "2009/9/1 11:46:53". % -spec get_textual_timestamp( timestamp() ) -> string(). get_textual_timestamp( { { Year, Month, Day }, { Hour, Minute, Second } } ) -> io_lib:format( "~B/~B/~B ~B:~2..0B:~2..0B", [ Year, Month, Day, Hour, Minute, Second ] ). % Returns a string corresponding to the current timestamp and able to be a part % of a path, like: "2010-11-18-at-13h-30m-35s.". % -spec get_textual_timestamp_for_path() -> string(). get_textual_timestamp_for_path() -> get_textual_timestamp_for_path( get_timestamp() ). % Returns a string corresponding to the specified timestamp and able to be a % part of a path, like: "2010-11-18-at-13h-30m-35s.". % -spec get_textual_timestamp_for_path( timestamp() ) -> string(). get_textual_timestamp_for_path( { { Year, Month, Day }, { Hour, Minute, Second } } ) -> io_lib:format( "~p-~p-~p-at-~Bh-~2..0Bm-~2..0Bs", [ Year, Month, Day, Hour, Minute, Second ] ). % Alias of get_textual_timestamp/1, defined for clarity. -spec timestamp_to_string( timestamp() ) -> string(). timestamp_to_string( Timestamp ) -> get_textual_timestamp( Timestamp ). % Parses back a timestamp in the form of "14/4/2011 18:48:51" into a % timestamp(), i.e. { _Date={Year,Month,Day}, _Time={Hour,Minute,Second} }. % -spec string_to_timestamp( string() ) -> timestamp(). string_to_timestamp( TimestampString ) -> case string:tokens( TimestampString, _Sep=" :/" ) of [ DayString, MonthString, YearString, HourString, MinuteString, SecondString ] -> Day = text_utils:string_to_integer( DayString ), Month = text_utils:string_to_integer( MonthString ), Year = text_utils:string_to_integer( YearString ), Hour = text_utils:string_to_integer( HourString ), Minute = text_utils:string_to_integer( MinuteString ), Second = text_utils:string_to_integer( SecondString ), { { Year, Month, Day }, { Hour, Minute, Second } }; _ -> throw( { timestamp_parsing_failed, TimestampString } ) end. % Returns the (signed) duration in seconds corresponding to the specified time. -spec get_duration( time_utils:time() ) -> unit_utils:seconds(). get_duration( { Hours, Minutes, Seconds } ) -> ( Hours * 60 + Minutes ) * 60 + Seconds. % Returns the (signed) duration in seconds between the two specified timestamps, % using the first one as starting time and the second one as stopping time. % -spec get_duration( timestamp(), timestamp() ) -> unit_utils:seconds(). get_duration( FirstTimestamp, SecondTimestamp ) -> First = calendar:datetime_to_gregorian_seconds( FirstTimestamp ), Second = calendar:datetime_to_gregorian_seconds( SecondTimestamp ), Second - First. % Returns the (signed) duration in seconds between the specified start timestamp % and the current time. % -spec get_duration_since( timestamp() ) -> unit_utils:seconds(). get_duration_since( StartTimestamp ) -> get_duration( StartTimestamp, get_timestamp() ). % Returns a textual description of the duration between the two specified % timestamps. % % See also: text_utils:duration_to_string/1, which is smarter. % -spec get_textual_duration( timestamp(), timestamp() ) -> string(). get_textual_duration( FirstTimestamp, SecondTimestamp ) -> { Days, { Hour, Minute, Second } } = calendar:seconds_to_daystime( get_duration( FirstTimestamp, SecondTimestamp ) ), lists:flatten( io_lib:format( "~B day(s), ~B hour(s), ~B minute(s) " "and ~B second(s)", [ Days, Hour, Minute, Second ] ) ). % Returns a timestamp that is as precise as possible: {MegaSecs,Secs,MicroSecs}, % where: % % - MegaSecs is an integer number of millions of seconds % % - Secs is an integer number of second which is less than one million % % - MicroSecs is an integer number of microseconds % -spec get_precise_timestamp() -> precise_timestamp(). get_precise_timestamp() -> % Was initially: erlang:now(). % os:timestamp() was then a bit lighter (not monotonic) % % Finally preferred (still not monotonic), since release 18.0: % erlang:timestamp(). % Returns the (signed) duration in milliseconds between the two specified % precise timestamps (as obtained thanks to get_precise_duration/0), using the % first one as starting time and the second one as stopping time. % -spec get_precise_duration( precise_timestamp(), precise_timestamp() ) -> unit_utils:milliseconds(). get_precise_duration( _FirstTimestamp={ A1, A2, A3 }, _SecondTimestamp={ B1, B2, B3 } ) -> % Seconds to be converted in milliseconds: 1000 * ( ( B1 - A1 ) * 1000000 + B2 - A2 ) + round( ( B3 - A3 ) / 1000 ). % Returns the (signed) duration in milliseconds between the specified precise % timestamp (as obtained thanks to get_precise_duration/0) and the current time. % -spec get_precise_duration_since( precise_timestamp() ) -> unit_utils:milliseconds(). get_precise_duration_since( StartTimestamp ) -> get_precise_duration( StartTimestamp, get_precise_timestamp() ). % Returns the date corresponding to the specified one augmented of the specified % number of days (possibly a negative number). % -spec get_date_after( date(), unit_utils:days() ) -> date(). get_date_after( BaseDate, Days ) -> DayCount = calendar:date_to_gregorian_days( BaseDate ) + Days, calendar:gregorian_days_to_date( DayCount ).