----------------------------------------------------------------------- -- util-dates-formats -- Date Format ala strftime -- Copyright (C) 2011, 2018, 2020, 2022 Stephane Carrez -- Written by Stephane Carrez (Stephane.Carrez@gmail.com) -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. ----------------------------------------------------------------------- with Ada.Containers; with Ada.Calendar; with Ada.Characters.Handling; with Ada.Calendar.Formatting; with GNAT.Calendar; with Util.Strings.Vectors; with Util.Strings.Transforms; package body Util.Dates.Formats is use Ada.Strings.Unbounded; type String_Array is array (Positive range <>) of Util.Strings.Name_Access; -- Week day names (long and short). Monday_Name : aliased constant String := "Monday"; Monday_Short_Name : aliased constant String := "Mon"; Tuesday_Name : aliased constant String := "Tuesday"; Tuesday_Short_Name : aliased constant String := "Tue"; Wednesday_Name : aliased constant String := "Wednesday"; Wednesday_Short_Name : aliased constant String := "Wed"; Thursday_Name : aliased constant String := "Thursday"; Thursday_Short_Name : aliased constant String := "Thu"; Friday_Name : aliased constant String := "Friday"; Friday_Short_Name : aliased constant String := "Fri"; Saturday_Name : aliased constant String := "Saturday"; Saturday_Short_Name : aliased constant String := "Sat"; Sunday_Name : aliased constant String := "Sunday"; Sunday_Short_Name : aliased constant String := "Sun"; -- Month names (long and short). January_Name : aliased constant String := "January"; January_Short_Name : aliased constant String := "Jan"; February_Name : aliased constant String := "February"; February_Short_Name : aliased constant String := "Feb"; March_Name : aliased constant String := "March"; March_Short_Name : aliased constant String := "Mar"; April_Name : aliased constant String := "April"; April_Short_Name : aliased constant String := "Apr"; May_Name : aliased constant String := "May"; May_Short_Name : aliased constant String := "May"; June_Name : aliased constant String := "June"; June_Short_Name : aliased constant String := "Jun"; July_Name : aliased constant String := "July"; July_Short_Name : aliased constant String := "Jul"; August_Name : aliased constant String := "August"; August_Short_Name : aliased constant String := "Aug"; September_Name : aliased constant String := "September"; September_Short_Name : aliased constant String := "Sep"; October_Name : aliased constant String := "October"; October_Short_Name : aliased constant String := "Oct"; November_Name : aliased constant String := "November"; November_Short_Name : aliased constant String := "Nov"; December_Name : aliased constant String := "December"; December_Short_Name : aliased constant String := "Dec"; Day_Names : constant String_Array (1 .. 7) := (Monday_Name'Access, Tuesday_Name'Access, Wednesday_Name'Access, Thursday_Name'Access, Friday_Name'Access, Saturday_Name'Access, Sunday_Name'Access); Day_Short_Names : constant String_Array (1 .. 7) := (Monday_Short_Name'Access, Tuesday_Short_Name'Access, Wednesday_Short_Name'Access, Thursday_Short_Name'Access, Friday_Short_Name'Access, Saturday_Short_Name'Access, Sunday_Short_Name'Access); Month_Names : constant String_Array (1 .. 12) := (January_Name'Access, February_Name'Access, March_Name'Access, April_Name'Access, May_Name'Access, June_Name'Access, July_Name'Access, August_Name'Access, September_Name'Access, October_Name'Access, November_Name'Access, December_Name'Access); Month_Short_Names : constant String_Array (1 .. 12) := (January_Short_Name'Access, February_Short_Name'Access, March_Short_Name'Access, April_Short_Name'Access, May_Short_Name'Access, June_Short_Name'Access, July_Short_Name'Access, August_Short_Name'Access, September_Short_Name'Access, October_Short_Name'Access, November_Short_Name'Access, December_Short_Name'Access); function Get_Label (Bundle : in Util.Properties.Manager'Class; Prefix : in String; Index : in Natural; Short : in Boolean) return String; function Get_Label (Bundle : in Util.Properties.Manager'Class; Prefix : in String; Index : in Natural; Short : in Boolean) return String is Num : constant String := Natural'Image (Index); Name : constant String := Prefix & Num (Num'First + 1 .. Num'Last); begin if Short then return Bundle.Get (Name & SHORT_SUFFIX, ""); else return Bundle.Get (Name & LONG_SUFFIX, ""); end if; end Get_Label; -- ------------------------------ -- Append the localized month string in the Into string. -- The month string is found in the resource bundle under the name: -- util.month.short -- util.month.long -- If the month string is not found, the month is displayed as a number. -- ------------------------------ procedure Append_Month (Into : in out Ada.Strings.Unbounded.Unbounded_String; Month : in Ada.Calendar.Month_Number; Bundle : in Util.Properties.Manager'Class; Short : in Boolean := True) is Value : constant String := Get_Label (Bundle, MONTH_NAME_PREFIX, Natural (Month), Short); begin if Value'Length > 0 then Append (Into, Value); elsif Short then Append (Into, Month_Short_Names (Month).all); else -- If the resource bundle is empty, fallback to hard-coded English values. Append (Into, Month_Names (Month).all); end if; end Append_Month; -- ------------------------------ -- Append the localized month string in the Into string. -- The month string is found in the resource bundle under the name: -- util.month.short -- util.month.long -- If the month string is not found, the month is displayed as a number. -- ------------------------------ procedure Append_Day (Into : in out Ada.Strings.Unbounded.Unbounded_String; Day : in Ada.Calendar.Formatting.Day_Name; Bundle : in Util.Properties.Manager'Class; Short : in Boolean := True) is use Ada.Calendar.Formatting; Value : constant String := Get_Label (Bundle, DAY_NAME_PREFIX, Day_Name'Pos (Day), Short); begin if Value'Length > 0 then Append (Into, Value); elsif Short then -- If the resource bundle is empty, fallback to hard-coded English values. Append (Into, Day_Short_Names (Day_Name'Pos (Day)).all); else Append (Into, Day_Names (Day_Name'Pos (Day)).all); end if; end Append_Day; -- ------------------------------ -- Append a number with padding if necessary -- ------------------------------ procedure Append_Number (Into : in out Ada.Strings.Unbounded.Unbounded_String; Value : in Natural; Padding : in Character; Length : in Natural := 2) is N : constant String := Natural'Image (Value); begin if Length = 0 or else (Padding /= ' ' and then Padding /= '0') then Append (Into, N (N'First + 1 .. N'Last)); elsif N'Length <= Length then Append (Into, Padding); Append (Into, N (N'First + 1 .. N'Last)); else Append (Into, N (N'Last - Length + 1 .. N'Last)); end if; end Append_Number; -- ------------------------------ -- Append the timezone offset -- ------------------------------ procedure Append_Time_Offset (Into : in out Ada.Strings.Unbounded.Unbounded_String; Offset : in Ada.Calendar.Time_Zones.Time_Offset) is use type Ada.Calendar.Time_Zones.Time_Offset; Value : Natural; begin if Offset < 0 then Append (Into, '-'); Value := Natural (-Offset); else Value := Natural (Offset); end if; Append_Number (Into, Value / 60, '0'); Append (Into, ':'); Append_Number (Into, Value mod 60, '0'); end Append_Time_Offset; -- ------------------------------ -- Format the date passed in Date using the date pattern specified in Pattern. -- For month and day of week strings, use the resource bundle passed in Bundle. -- Returns the formatted date in the stream. -- ------------------------------ procedure Format (Into : in out Ada.Strings.Unbounded.Unbounded_String; Pattern : in String; Date : in Ada.Calendar.Time; Bundle : in Util.Properties.Manager'Class) is TM : Date_Record; begin Split (TM, Date); Format (Into, Pattern, TM, Bundle); end Format; -- ------------------------------ -- Format the date passed in Date using the date pattern specified in Pattern. -- For month and day of week strings, use the resource bundle passed in Bundle. -- Returns the formatted date in the stream. -- ------------------------------ procedure Format (Into : in out Ada.Strings.Unbounded.Unbounded_String; Pattern : in String; Date : in Date_Record; Bundle : in Util.Properties.Manager'Class) is use Ada.Calendar; use Ada.Calendar.Formatting; use Util.Strings.Transforms; use type Ada.Calendar.Time_Zones.Time_Offset; Pos : Positive := Pattern'First; Pad : Character := '0'; C : Character; begin while Pos <= Pattern'Last loop C := Pattern (Pos); if C /= '%' then Append (Into, C); Pos := Pos + 1; else Pos := Pos + 1; exit when Pos > Pattern'Last; C := Pattern (Pos); Pad := '0'; if C in '_' | '-' | 'E' | 'O' | '^' then exit when Pos = Pattern'Last; if C = '-' then Pad := '-'; elsif C = '_' then Pad := ' '; end if; Pos := Pos + 1; C := Pattern (Pos); end if; case C is when '%' => Append (Into, '%'); -- %a The abbreviated weekday name according to the current locale. when 'a' => Append_Day (Into, Date.Day, Bundle, True); -- %A The full weekday name according to the current locale. when 'A' => Append_Day (Into, Date.Day, Bundle, False); -- %b The abbreviated month name according to the current locale. -- %h Equivalent to %b. (SU) when 'b' | 'h' => Append_Month (Into, Date.Month, Bundle, True); -- %B The full month name according to the current locale. when 'B' => Append_Month (Into, Date.Month, Bundle, False); -- %c The preferred date and time representation for the current locale. when 'c' => Format (Into, Bundle.Get (DATE_TIME_LOCALE_NAME, DATE_TIME_DEFAULT_PATTERN), Date, Bundle); -- %C The century number (year/100) as a 2-digit integer. (SU) when 'C' => Append_Number (Into, Natural (Date.Year / 100), Pad); -- %d The day of the month as a decimal number (range 01 to 31). when 'd' => Append_Number (Into, Natural (Date.Month_Day), Pad); -- %D Equivalent to %m/%d/%y when 'D' => Format (Into, "%m/%d/%y", Date, Bundle); -- %e Like %d, the day of the month as a decimal number, -- but a leading zero is replaced by a space. (SU) when 'e' => Append_Number (Into, Natural (Date.Month), ' '); -- %F Equivalent to %Y-%m-%d (the ISO 8601 date format). (C99) when 'F' => Format (Into, "%Y-%m-%d", Date, Bundle); -- %G The ISO 8601 week-based year when 'G' => Append_Number (Into, Natural (Date.Year), Pad, 4); Append (Into, 'W'); Append_Number (Into, Natural (GNAT.Calendar.Week_In_Year (Date.Date)), Pad); -- %g Like %G, but without century, that is, -- with a 2-digit year (00-99). (TZ) when 'g' => Append_Number (Into, Natural (Date.Year mod 100), Pad, 2); Append (Into, 'W'); Append_Number (Into, Natural (GNAT.Calendar.Week_In_Year (Date.Date)), Pad); -- %H The hour as a decimal number using a 24-hour clock (range 00 to 23). when 'H' => Append_Number (Into, Natural (Date.Hour), Pad); -- %I The hour as a decimal number using a 12-hour clock (range 01 to 12). when 'I' => Append_Number (Into, Natural (Date.Hour mod 12), Pad); -- %j The day of the year as a decimal number (range 001 to 366). when 'j' => Append_Number (Into, Natural (GNAT.Calendar.Day_In_Year (Date.Date)), Pad, 3); -- %k The hour (24-hour clock) as a decimal number (range 0 to 23); when 'k' => Append_Number (Into, Natural (Date.Hour), ' '); -- %l The hour (12-hour clock) as a decimal number (range 1 to 12); when 'l' => Append_Number (Into, Natural (Date.Hour mod 12), ' '); -- %m The month as a decimal number (range 01 to 12). when 'm' => Append_Number (Into, Natural (Date.Month), Pad); -- %M The minute as a decimal number (range 00 to 59). when 'M' => Append_Number (Into, Natural (Date.Minute), Pad); -- %n A newline character. (SU) when 'n' => Append (Into, ASCII.LF); -- %p Either "AM" or "PM" when 'p' => if Date.Hour >= 12 then Append (Into, Bundle.Get (PM_NAME, PM_DEFAULT)); else Append (Into, Bundle.Get (AM_NAME, AM_DEFAULT)); end if; -- %P Like %p but in lowercase: "am" or "pm" when 'P' => -- SCz 2011-10-01: the To_Lower_Case will not work for UTF-8 strings. if Date.Hour >= 12 then Append (Into, To_Lower_Case (Bundle.Get (PM_NAME, PM_DEFAULT))); else Append (Into, To_Lower_Case (Bundle.Get (AM_NAME, AM_DEFAULT))); end if; -- %r The time in a.m. or p.m. notation. -- In the POSIX locale this is equivalent to %I:%M:%S %p. (SU) when 'r' => Format (Into, "%I:%M:%S %p", Date, Bundle); -- %R The time in 24-hour notation (%H:%M). when 'R' => Format (Into, "%H:%M", Date, Bundle); -- %s The number of seconds since the Epoch, that is, -- since 1970-01-01 00:00:00 UTC. (TZ) when 's' => null; -- %S The second as a decimal number (range 00 to 60). when 'S' => Append_Number (Into, Natural (Date.Second), Pad); -- %t A tab character. (SU) when 't' => Append (Into, ASCII.HT); -- %T The time in 24-hour notation (%H:%M:%S). (SU) when 'T' => Format (Into, "%H:%M:%S", Date, Bundle); -- %u The day of the week as a decimal, range 1 to 7, -- Monday being 1. See also %w. (SU) when 'u' => Append_Number (Into, Day_Name'Pos (Date.Day), Pad); -- %U The week number of the current year as a decimal number, -- range 00 to 53 when 'U' => Append_Number (Into, Natural (GNAT.Calendar.Week_In_Year (Date.Date)), Pad); -- %V The ISO 8601 week number when 'V' => Append_Number (Into, Natural (GNAT.Calendar.Week_In_Year (Date.Date)), Pad); -- %w The day of the week as a decimal, range 0 to 6, Sunday being 0. -- See also %u. when 'w' => if Date.Day = Sunday then Append_Number (Into, 0, Pad); else Append_Number (Into, Day_Name'Pos (Date.Day) + 1, Pad); end if; -- %W The week number of the current year as a decimal number, -- range 00 to 53 when 'W' => Append_Number (Into, Natural (GNAT.Calendar.Week_In_Year (Date.Date)), Pad); -- %x The preferred date representation for the current locale without -- the time. when 'x' => Format (Into, Bundle.Get (DATE_LOCALE_NAME, DATE_DEFAULT_PATTERN), Date, Bundle); -- %X The preferred time representation for the current locale without -- the date. when 'X' => Format (Into, Bundle.Get (TIME_LOCALE_NAME, TIME_DEFAULT_PATTERN), Date, Bundle); -- %y The year as a decimal number without a century (range 00 to 99). when 'y' => Append_Number (Into, Natural (Date.Year mod 100), Pad); -- %Y The year as a decimal number including the century. when 'Y' => Append_Number (Into, Natural (Date.Year), Pad, 4); -- %z The time-zone as hour offset from GMT. when 'z' => Append_Time_Offset (Into, Date.Time_Zone); -- %Z The timezone or name or abbreviation. when 'Z' => Append (Into, "UTC"); if Date.Time_Zone > 0 then Append (Into, '+'); Append_Time_Offset (Into, Date.Time_Zone); elsif Date.Time_Zone < 0 then Append_Time_Offset (Into, Date.Time_Zone); end if; when others => Append (Into, '%'); Append (Into, Pattern (Pos)); end case; Pos := Pos + 1; end if; end loop; end Format; function Format (Pattern : in String; Date : in Ada.Calendar.Time; Bundle : in Util.Properties.Manager'Class) return String is Result : Unbounded_String; begin Format (Result, Pattern, Date, Bundle); return To_String (Result); end Format; procedure Parse (Date : in String; Pattern : in String; Bundle : in Util.Properties.Manager'Class; Result : in out Date_Record; Last : out Positive); function Parse (Date : in String; Pattern : in String; Bundle : in Util.Properties.Manager'Class) return Date_Record is Last : Positive; Result : Date_Record; begin Parse (Date, Pattern, Bundle, Result, Last); if Last <= Date'Last then raise Constraint_Error with "Invalid date format"; end if; Result.Date := Time_Of (Result); return Result; end Parse; -- ------------------------------ -- Format the date passed in Date using the date pattern specified in Pattern. -- For month and day of week strings, use the resource bundle passed in Bundle. -- Returns the formatted date in the stream. -- ------------------------------ procedure Parse (Date : in String; Pattern : in String; Bundle : in Util.Properties.Manager'Class; Result : in out Date_Record; Last : out Positive) is use Ada.Calendar; use Ada.Calendar.Formatting; use Ada.Calendar.Time_Zones; use Util.Strings.Transforms; use type Ada.Containers.Count_Type; procedure Expect (C : in Character); function Expect (List : in Util.Strings.Vectors.Vector) return Natural; function Parse_Number (Min : in Natural; Max : in Natural) return Natural; procedure Load (List : in out Util.Strings.Vectors.Vector; First : in Natural; Last : in Natural; Prefix : in String; Short : in Boolean; Default : in String_Array); function Parse_Short_Day return Formatting.Day_Name; function Parse_Long_Day return Formatting.Day_Name; function Parse_Short_Month return Month_Number; function Parse_Long_Month return Month_Number; function Check_Match (Value : in String; Upper : in Boolean) return Boolean; procedure Parse_AM_PM (Upper : in Boolean); Pattern_Pos : Positive := Pattern'First; Pos : Natural := Date'First; C : Character; Short_Months : Util.Strings.Vectors.Vector; Long_Months : Util.Strings.Vectors.Vector; Short_Days : Util.Strings.Vectors.Vector; Long_Days : Util.Strings.Vectors.Vector; procedure Expect (C : in Character) is begin if Date (Pos) /= C then raise Constraint_Error with "Invalid date format at" & Natural'Image (Pos); end if; Pos := Pos + 1; end Expect; function Expect (List : in Util.Strings.Vectors.Vector) return Natural is Index : Natural := 0; begin for S of List loop if Pos + S'Length - 1 <= Date'Last and then S = Date (Pos .. Pos + S'Length - 1) then Pos := Pos + S'Length; return Index; end if; Index := Index + 1; end loop; raise Constraint_Error with "Invalid date format at" & Natural'Image (Pos); end Expect; procedure Load (List : in out Util.Strings.Vectors.Vector; First : in Natural; Last : in Natural; Prefix : in String; Short : in Boolean; Default : in String_Array) is Offset : constant Natural := Default'First - First; begin if List.Length = 0 then for I in First .. Last loop declare Name : constant String := Get_Label (Bundle, Prefix, I, Short); begin if Name'Length = 0 then List.Append (Default (I + Offset).all); else List.Append (Name); end if; end; end loop; end if; end Load; function Parse_Short_Day return Formatting.Day_Name is begin Load (Short_Days, 0, 6, DAY_NAME_PREFIX, True, Day_Short_Names); return Formatting.Day_Name'Val (Expect (Short_Days)); end Parse_Short_Day; function Parse_Long_Day return Formatting.Day_Name is begin Load (Long_Days, 0, 6, DAY_NAME_PREFIX, False, Day_Names); return Formatting.Day_Name'Val (Expect (Long_Days)); end Parse_Long_Day; function Parse_Short_Month return Month_Number is begin Load (Short_Months, 1, 12, MONTH_NAME_PREFIX, True, Month_Short_Names); return Month_Number (Expect (Short_Months) + 1); end Parse_Short_Month; function Parse_Long_Month return Month_Number is begin Load (Long_Months, 1, 12, MONTH_NAME_PREFIX, False, Month_Names); return Month_Number (Expect (Long_Months) + 1); end Parse_Long_Month; function Parse_Number (Min : in Natural; Max : in Natural) return Natural is Value : Natural := 0; begin if Date (Pos) < '0' or else Date (Pos) > '9' then raise Constraint_Error with "Invalid date format: expecting integer"; end if; while (10 * Value) < Max and then Pos <= Date'Last and then Date (Pos) >= '0' and then Date (Pos) <= '9' loop Value := Value * 10; Value := Value + Character'Pos (Date (Pos)) - Character'Pos ('0'); Pos := Pos + 1; end loop; if Value < Min or else Value > Max then raise Constraint_Error with "Invalid date format: out of range"; end if; return Value; end Parse_Number; function Check_Match (Value : in String; Upper : in Boolean) return Boolean is begin if Pos + Value'Length - 1 > Date'Last then return False; elsif Upper then return Date (Pos .. Pos + Value'Length - 1) = Value; else return Date (Pos .. Pos + Value'Length - 1) = To_Lower_Case (Value); end if; end Check_Match; procedure Parse_AM_PM (Upper : in Boolean) is AM : constant String := Bundle.Get (AM_NAME, AM_DEFAULT); begin if Check_Match (AM, Upper) then Pos := Pos + AM'Length; else declare PM : constant String := Bundle.Get (PM_NAME, PM_DEFAULT); begin if Check_Match (PM, Upper) then Pos := Pos + PM'Length; Result.Hour := Result.Hour + 12; else raise Constraint_Error with "Invalid date format: expecting am or pm"; end if; end; end if; end Parse_AM_PM; Century : Integer := -1; Value : Integer; Week_Number : Integer := -1; pragma Unreferenced (Week_Number, Century); begin while Pattern_Pos <= Pattern'Last and then Pos <= Date'Last loop C := Pattern (Pattern_Pos); if C = ' ' then Pattern_Pos := Pattern_Pos + 1; while Pos <= Date'Last and then Ada.Characters.Handling.Is_Space (Date (Pos)) loop Pos := Pos + 1; end loop; elsif C /= '%' then Expect (C); Pattern_Pos := Pattern_Pos + 1; else Pattern_Pos := Pattern_Pos + 1; exit when Pattern_Pos > Pattern'Last; C := Pattern (Pattern_Pos); if C in '_' | '-' | 'E' | 'O' | '^' then exit when Pattern_Pos = Pattern'Last; Pattern_Pos := Pattern_Pos + 1; C := Pattern (Pattern_Pos); end if; case C is when '%' => Expect ('%'); -- %a The abbreviated weekday name according to the current locale. when 'a' => Result.Day := Parse_Short_Day; -- %A The full weekday name according to the current locale. when 'A' => Result.Day := Parse_Long_Day; -- %b The abbreviated month name according to the current locale. -- %h Equivalent to %b. (SU) when 'b' | 'h' => Result.Month := Parse_Short_Month; -- %B The full month name according to the current locale. when 'B' => Result.Month := Parse_Long_Month; -- %c The preferred date and time representation for the current locale. when 'c' => Parse (Date (Pos .. Date'Last), Bundle.Get (DATE_TIME_LOCALE_NAME, DATE_TIME_DEFAULT_PATTERN), Bundle, Result, Pos); -- %C The century number (year/100) as a 2-digit integer. (SU) when 'C' => Century := 100 * Parse_Number (Min => 1, Max => 99); -- %d The day of the month as a decimal number (range 01 to 31). when 'd' => Result.Month_Day := Day_Number (Parse_Number (Min => 1, Max => 31)); -- %D Equivalent to %m/%d/%y when 'D' => Parse (Date (Pos .. Date'Last), "%m/%d/%y", Bundle, Result, Pos); -- %e Like %d, the day of the month as a decimal number, -- but a leading zero is replaced by a space. (SU) when 'e' => Result.Month := Month_Number (Parse_Number (Min => 1, Max => 31)); -- %F Equivalent to %Y-%m-%d (the ISO 8601 date format). (C99) when 'F' => Parse (Date (Pos .. Date'Last), "%Y-%m-%d", Bundle, Result, Pos); -- %G The ISO 8601 week-based year when 'G' => Parse (Date (Pos .. Date'Last), "%YW%W", Bundle, Result, Pos); -- %g Like %G, but without century, that is, -- with a 2-digit year (00-99). (TZ) when 'g' => Parse (Date (Pos .. Date'Last), "%yW%W", Bundle, Result, Pos); -- %H The hour as a decimal number using a 24-hour clock (range 00 to 23). when 'H' => Result.Hour := Formatting.Hour_Number (Parse_Number (Min => 1, Max => 23)); -- %I The hour as a decimal number using a 12-hour clock (range 01 to 12). when 'I' => Result.Hour := Formatting.Hour_Number (Parse_Number (Min => 1, Max => 12)); -- %j The day of the year as a decimal number (range 001 to 366). when 'j' => Value := Parse_Number (Min => 1, Max => 366); -- %k The hour (24-hour clock) as a decimal number (range 0 to 23); when 'k' => Result.Hour := Hour_Number (Parse_Number (Min => 0, Max => 23)); -- %l The hour (12-hour clock) as a decimal number (range 1 to 12); when 'l' => Result.Hour := Parse_Number (Min => 1, Max => 12); -- %m The month as a decimal number (range 01 to 12). when 'm' => Result.Month := Month_Number (Parse_Number (Min => 1, Max => 12)); -- %M The minute as a decimal number (range 00 to 59). when 'M' => Result.Minute := Minute_Number (Parse_Number (Min => 0, Max => 59)); -- %n A newline character. (SU) when 'n' => Expect (ASCII.LF); -- %p Either "AM" or "PM" when 'p' => Parse_AM_PM (Upper => True); -- %P Like %p but in lowercase: "am" or "pm" when 'P' => Parse_AM_PM (Upper => False); -- %r The time in a.m. or p.m. notation. -- In the POSIX locale this is equivalent to %I:%M:%S %p. (SU) when 'r' => Parse (Date (Pos .. Date'Last), "%I:%M:%S %p", Bundle, Result, Pos); -- %R The time in 24-hour notation (%H:%M). when 'R' => Parse (Date (Pos .. Date'Last), "%H:%M", Bundle, Result, Pos); -- %s The number of seconds since the Epoch, that is, -- since 1970-01-01 00:00:00 UTC. (TZ) when 's' => null; -- %S The second as a decimal number (range 00 to 60). when 'S' => Result.Second := Formatting.Second_Number (Parse_Number (Min => 0, Max => 59)); -- %t A tab character. (SU) when 't' => Expect (ASCII.HT); -- %T The time in 24-hour notation (%H:%M:%S). (SU) when 'T' => Parse (Date (Pos .. Date'Last), "%H:%M:%S", Bundle, Result, Pos); -- %u The day of the week as a decimal, range 1 to 7, -- Monday being 1. See also %w. (SU) when 'u' => Result.Day := Day_Name'Val (Parse_Number (Min => 1, Max => 7)); -- %U The week number of the current year as a decimal number, -- range 00 to 53 when 'U' => Week_Number := Parse_Number (Min => 0, Max => 53); -- %V The ISO 8601 week number when 'V' => Week_Number := Parse_Number (Min => 0, Max => 53); -- %w The day of the week as a decimal, range 0 to 6, Sunday being 0. -- See also %u. when 'w' => Value := Parse_Number (Min => 0, Max => 6); if Value = 0 then Result.Day := Sunday; else Result.Day := Day_Name'Val (Value - 1); end if; -- %W The week number of the current year as a decimal number, -- range 00 to 53 when 'W' => Week_Number := Parse_Number (Min => 0, Max => 53); -- %x The preferred date representation for the current locale without -- the time. when 'x' => Parse (Date (Pos .. Date'Last), Bundle.Get (DATE_LOCALE_NAME, DATE_DEFAULT_PATTERN), Bundle, Result, Pos); -- %X The preferred time representation for the current locale without -- the date. when 'X' => Parse (Date (Pos .. Date'Last), Bundle.Get (TIME_LOCALE_NAME, TIME_DEFAULT_PATTERN), Bundle, Result, Pos); -- %y The year as a decimal number without a century (range 00 to 99). when 'y' => Value := Parse_Number (Min => 0, Max => 99); Value := Value + (Natural (Result.Year) / 100) * 100; Result.Year := Year_Number (Value); -- %Y The year as a decimal number including the century. when 'Y' => Result.Year := Parse_Number (Min => 1900, Max => 9999); -- %z The time-zone as hour offset from GMT. when 'z' => if Date (Pos) = '+' then Pos := Pos + 1; Value := Parse_Number (Min => 0, Max => 12); Result.Time_Zone := Time_Offset (Value * 60); Expect (':'); Value := Parse_Number (Min => 0, Max => 59); Result.Time_Zone := Result.Time_Zone + Time_Offset (Value); elsif Date (Pos) = '-' then Pos := Pos + 1; Value := Parse_Number (Min => 0, Max => 12); Result.Time_Zone := -Time_Offset (Value * 60); Expect (':'); Value := Parse_Number (Min => 0, Max => 59); Result.Time_Zone := Result.Time_Zone - Time_Offset (Value); else raise Constraint_Error with "Invalid date format"; end if; -- %Z The timezone or name or abbreviation. when 'Z' => Expect ('U'); Expect ('T'); Expect ('C'); if Date (Pos) = '+' then Pos := Pos + 1; Value := Parse_Number (Min => 0, Max => 12); Result.Time_Zone := Time_Offset (Value * 60); Expect (':'); Value := Parse_Number (Min => 0, Max => 59); Result.Time_Zone := Result.Time_Zone + Time_Offset (Value); elsif Date (Pos) = '-' then Pos := Pos + 1; Value := Parse_Number (Min => 0, Max => 12); Result.Time_Zone := -Time_Offset (Value * 60); Expect (':'); Value := Parse_Number (Min => 0, Max => 59); Result.Time_Zone := Result.Time_Zone - Time_Offset (Value); else Result.Time_Zone := 0; end if; when others => Expect ('%'); Expect (Pattern (Pattern_Pos)); end case; Pattern_Pos := Pattern_Pos + 1; end if; end loop; Last := Pos; if Pattern_Pos <= Pattern'Last then raise Constraint_Error with "Invalid date format: incomplete date"; end if; end Parse; end Util.Dates.Formats;