1 Introduction
2 Ground Rules

Building a File System
3 File Systems
4 File Content Data Structure
5 Allocation Cluster Manager
6 Exceptions and Emancipation
7 Base Classes, Testing, and More
8 File Meta Data
9 Native File Class
10 Our File System
11 Allocation Table
12 File System Support Code
13 Initializing the File System
14 Contiguous Files
15 Rebuilding the File System
16 Native File System Support Methods
17 Lookups, Wildcards, and Unicode, Oh My
18 Finishing the File System Class

The Init Program
19 Hardware Abstraction and UOS Architecture
20 Init Command Mode
21 Using Our File System
22 Hardware and Device Lists
23 Fun with Stores: Partitions
24 Fun with Stores: RAID
25 Fun with Stores: RAM Disks
26 Init wrap-up

The Executive
27 Overview of The Executive
28 Starting the Kernel
29 The Kernel
30 Making a Store Bootable
31 The MMC
32 The HMC
33 Loading the components
34 Using the File Processor
35 Symbols and the SSC
36 The File Processor and Device Management
37 The File Processor and File System Management
38 Finishing Executive Startup

Users and Security
39 Introduction to Users and Security
40 More Fun With Stores: File Heaps
41 File Heaps, part 2
42 SysUAF
43 TUser
44 SysUAF API

Terminal I/O
45 Shells and UCL
46 UOS API, the Application Side
47 UOS API, the Executive Side
48 I/O Devices
49 Streams
50 Terminal Output Filters
51 The TTerminal Class
52 Handles
53 Putting it All Together
54 Getting Terminal Input
55 QIO
56 Cooking Terminal Input
57 Putting it all together, part 2
58 Quotas and I/O

UCL
59 UCL Basics
60 Symbol Substitution
61 UCL Command execution
62 UCL Command execution, part 2
63 UCL Command Abbreviation
64 ASTs
65 UCL Expressions, Part 1
66 UCL Expressions, Part 2: Support code
67 UCL Expressions, part 3: Parsing
68 SYS_GETJPIW and SYS_TRNLNM
69 UCL Expressions, part 4: Evaluation

UCL Lexical Functions
70 PROCESS_SCAN
71 PROCESS_SCAN, Part 2
72 TProcess updates
73 Unicode revisted
74 Lexical functions: F$CONTEXT
75 Lexical functions: F$PID
76 Lexical Functions: F$CUNITS
77 Lexical Functions: F$CVSI and F$CVUI
78 UOS Date and Time Formatting
79 Lexical Functions: F$CVTIME
80 LIB_CVTIME
81 Date/Time Contexts
82 SYS_GETTIM, LIB_Get_Timestamp, SYS_ASCTIM, and LIB_SYS_ASCTIM
83 Lexical Functions: F$DELTA_TIME
84 Lexical functions: F$DEVICE
85 SYS_DEVICE_SCAN
86 Lexical functions: F$DIRECTORY
87 Lexical functions: F$EDIT and F$ELEMENT
88 Lexical functions: F$ENVIRONMENT
89 SYS_GETUAI
90 Lexical functions: F$EXTRACT and F$IDENTIFIER
91 LIB_FAO and LIB_FAOL
92 LIB_FAO and LIB_FAOL, part 2
93 Lexical functions: F$FAO
94 File Processing Structures
95 Lexical functions: F$FILE_ATTRIBUTES
96 SYS_DISPLAY
97 UCL Lexical functions: F$GETDVI
98 Parse_GetDVI

Glossary/Index


Download sources
Download binaries

LIB_CVTIME

In the previous article, we examined the F$CVTIME lexical function, which is merely a UCL wrapper for the LIB_CVTIME function. We will address this in a bit, but first we need to talk about UOS time support.

In VMS, time support routines are placed in the VMS executive. In UOS, all date and time functions (other than to get or set the system time) are placed in Starlet, which is ring 3 code. Remember, our philosophy is to minimize what is in the UOS executive. We decided that the only things that need to be in the executive are those things which we need to protect from malicious or flawed code, or which must be centralized rather than be handled on a per-process basis. This approach means that interpretation and formatting of date and time does not belong in the executive. Frankly, I think it is bad design for it to be in the VMS executive, but no one asked me. Starlet is part of UOS and contains all of the date and time handling code that can be used by any program. As mentioned above, only the actually setting and getting of the current system time is enscounced in the executive. Thus, all programs can use a standardized means of dealing with dates and times, but the actual code resides in a library that is linked to programs in the application ring.

As a consequence, several VMS system routines are actually handled by starlet in UOS. For compatibility with VMS, the Sys unit will include these VMS system calls, but they will be redirected to Starlet.

const CVF_Absolute = 0 ; // Absolute date/time output format
const CVF_Comparison = 1 ; // yyyy-mm-dd hh:mm:ss.cc output format
const CVF_Delta = 2 ; // Delta format

const CVO_DateTime = 0 ;
const CVO_Date = 1 ;
const CVO_Time = 2 ;
const CVO_Hour = 3 ;
const CVO_Second = 4 ;
const CVO_Minute = 5 ;
const CVO_Hundredth = 6 ;
const CVO_Day = 7 ;
const CVO_Month = 8 ;
const CVO_Weekday = 9 ;
const CVO_Year = 10 ;
const CVO_DayofYear = 11 ;
const CVO_HourofYear = 12 ;
const CVO_MinuteofYear = 13 ;
const CVO_SecondofYear = 14 ;

const DTF_Defaulted_Year = 1 ; // The year was defaulted (omitted in input string)
const DTF_Defaulted_Month = 2 ;
const DTF_Defaulted_Day = 4 ;
const DTF_Defaulted_Hour = 8 ;
const DTF_Defaulted_Minute = 16 ;
const DTF_Defaulted_Second = 32 ;
const DTF_Defaulted_Tenths = 64 ;
const DTF_Defaulted_Meridium = 128 ;
const DTF_Negative = 256 ; // Delta date is negative (past)
const DTF_Absolute = 512 ; // Absolute date supplied
const DTF_Delta = 1024 ; // Delta date supplied
const DTF_Error = 2048 ; // Error in input format specification

type TDTSS = packed record
                 Quoted : boolean ;
                 Last_Valid_Position : integer ;
                 Year : word ;
                 Month, Day, Hour, Minute, Second : byte ;
                 Billionths : int64 ;
                 Meridium : byte ;
                 Weekday : byte ;
                 Delta_Days, Delta_Hours, Delta_Minutes, Delta_Seconds : word ;
                 Delta_Billionths : int64 ;
                 Relative : byte ; // 0 = none, 1 = yesterday, 2 = today, 3=tomorrow
                 Flags : integer ; // See DTF_*
             end ;
The CVF_ and CVO_ constants are used by the LIB_CVTIME function, which we'll discuss shortly. The DTF_ constants are used to as flags for the Flags item in the TDTSS record. This structure is used to store the results of parsing a date/time specification.

function LIB_CVTime( var Time : string ; Format, Output : word ) : cardinal ;

var DTSS : TDTSS ;
    Timestamp : int64 ;
    Year_Start_Timestamp : int64 ;
    Y, Mo, D, H, M, Sec : word ;
    NS : longint ;
    SRB : TSRB ;
    Len : int64 ;

begin
    fillchar( DTSS, sizeof( DTSS ), 0 ) ;
    if( Time = '' ) then
    begin
        Timestamp := LIB_Get_Timestamp ; // Default to current time
        DTSS.Flags := DTF_Absolute ; // Absolute date supplied
        Parse_Sirius_Timestamp( TimeStamp, Y, Mo, D, H, M, Sec, NS ) ;
        DTSS.Year := Y ;
        DTSS.Month := Mo ;
        DTSS.Day := D ;
        DTSS.Hour := H ;
        DTSS.Minute := M ;
        DTSS.Second := Sec ;
        DTSS.Billionths := NS ;
    end else
    begin
        LIB_Parse_Date_Time( 0, Time, DTSS ) ;
    end ;
First, we zero the DTSS struture. If no time was passed, we get the current time via LIB_Get_Timestamp, parse the timestamp via Parse_Sirius_Timestamp, and set the various DTSS fields appropriately. If a time value is passed, we call the LIB_Parse_Date_Time function to fill DTSS. We will discuss LIB_Get_Timestamp in another article and LIB_Parse_Date_Time later in this article.

    if( Output >= CVO_DayofYear ) then
    begin
        Year_Start_Timestamp := Encode_Sirius_Timestamp( DTSS.Year, 1, 1, 0, 0, 0, 0 ) ;
    end ;
If the requested output is Dayofyear, HourofYear, MinuteofYear, or SecondofYear, we need to have the timestamp of the beginning of the year in order to make the calculation.

    if( Format = CVF_Delta ) then // Requested delta format
    begin
        if( Time = '' ) then
        begin
            fillchar( DTSS, sizeof( DTSS ), 0 ) ;
        end else
        if( ( DTSS.Flags and DTF_Delta ) = 0 ) then // Supplied date wasn't delta
        begin
            Result := LIB_EVDTIME ;
            exit ;
        end else
        if( Output >= CVO_Day ) then
        begin
            Result := LIB_BADTOPT ;
            exit ;
        end ;
    end ;
    if( ( DTSS.Flags and DTF_Delta ) <> 0 ) then // Supplied date was delta
    begin
        if( Format <> CVF_Delta ) then // Did not request delta format
        begin
            Result := LIB_EVDTIME ;
            exit ;
        end ;
    end ;
If delta format is requested, and the input date wasn't delta, we return with an error. If the requested output is Dayofyear, HourofYear, MinuteofYear, or SecondofYear, we return an error, because those make no sense for a delta time. If the requested format is delta and no input time is specified, it is treated as a delta offset from the current date/time, which is a delta value of +0:00:00:00.00, so we zero the DTSS fields. If the supplied date was a delta but the requested format wasn't delta, we return an error.

    Time := '' ;
    case Output of
        CVO_DateTime, CVO_Date, CVO_Time : ; // Handled later
        CVO_Hour : Time := inttostr( DTSS.Hour ) ;
        CVO_Second : Time := inttostr( DTSS.Second ) ;
        CVO_Minute : Time := inttostr( DTSS.Minute ) ;
        CVO_Hundredth : Time := inttostr( DTSS.Billionths div 10000000 ) ;
        CVO_Day : Time := inttostr( DTSS.Day ) ;
        CVO_Month : Time := inttostr( DTSS.Month ) ;
        CVO_Weekday : 
            Time := Default_Time_Context.Parsed_List( LIB_K_WEEKDAY_NAME_C )[ ( ( Timestamp div Day_Value ) 
            - 1 ) mod 7 ] ;
        CVO_Year : Time := inttostr( DTSS.Year ) ;
        CVO_DayofYear : Time := inttostr( ( Timestamp - Year_Start_Timestamp ) div Day_Value ) ;
        CVO_HourofYear : Time := inttostr( ( Timestamp - Year_Start_Timestamp ) div Hour_Value ) ;
        CVO_MinuteofYear : Time := inttostr( ( Timestamp - Year_Start_Timestamp ) div Minute_Value ) ;
        CVO_SecondofYear : Time := inttostr( ( Timestamp - Year_Start_Timestamp ) div Second_Value ) ;
        else
            begin
                Result := LIB_EVDTIME ;
                exit ;
            end ;
    end ;
Now that the preliminaries are out of the way, we set the function result to null. Then, based on the requested output format, we do the appropriate processing. The Datetime, Date, and Time formats are handled later. For second, minute, hour, day, month, year, or hundreths, we simply return the appropriate value from DTSS. For the weekday, we take the timestamp and divide by the number of nanoseconds in a day. This gives us the number of days since Jan 1, year 0. We then mod (division remainer) the time by 7. Projecting the Gregorian date back, the first day of year 0 is Sunday. So we can use the calculated value in the parsed_List value of the Default_Time_Context. Note that we subtract 1 to convert from a 0-based day to a 1-based offset into the parsed list. Otherwise we calculate offsets from the beginning of the year, as appropriate. We will discuss Time contexts and the Default_Time_Context function in the next article.

    if( Format = CVF_Absolute ) then // Absolute date/time output format
    begin
        case Output of
            CVO_DateTime : Time := Get_Absolute( 0, Result ) ;
            CVO_Date : Time := Get_Absolute( 2, Result ) ;
            CVO_Time : Time := Get_Absolute( 1, Result ) ;
        end ;
    end else
If the requested output format is Absolute, and the output time is DateTime, Date, or Time, we get the absolute time. We'll discuss Get_Absolute later in this article.

    if( Format = CVF_Comparison ) then // yyyy-mm-dd hh:mm:ss.cc output format
    begin
        case Output of
            CVO_DateTime : Time := LPad( inttostr( DTSS.Year ), 4, '0' ) + '-' +
                                   LPad( inttostr( DTSS.Month ), 2, '0' ) + '-' +
                                   LPad( inttostr( DTSS.Day ), 2, '0' ) + ' ' +
                                   LPad( inttostr( DTSS.Hour ), 2, '0' ) + ':' +
                                   LPad( inttostr( DTSS.Minute ), 2, '0' ) + ':' +
                                   LPad( inttostr( DTSS.Second ), 2, '0' ) + '.' +
                                   copy( LPad( inttostr( DTSS.Billionths ), 9, '0' ), 1, 2 ) ;
            CVO_Date : Time := LPad( inttostr( DTSS.Year ), 4, '0' ) + '-' +
                                   LPad( inttostr( DTSS.Month ), 2, '0' ) + '-' +
                                   LPad( inttostr( DTSS.Day ), 2, '0' ) ;
            CVO_Time : Time := LPad( inttostr( DTSS.Hour ), 2, '0' ) + ':' +
                                   LPad( inttostr( DTSS.Minute ), 2, '0' ) + ':' +
                                   LPad( inttostr( DTSS.Second ), 2, '0' ) + '.' +
                                   copy( LPad( inttostr( DTSS.Billionths ), 9, '0' ), 1, 2 ) ;
        end ;
    end else
If the requested output format is comparison, and the output time is DateTime, Date, or Time, we build the comparison time from the DTSS fields.

    if( Format = CVF_Delta ) then // Delta format
    begin
        case Output of
            CVO_Hour : Time := '+0:' + inttostr( DTSS.Hour ) ;
            CVO_Second : Time := '+0:0:0:' + inttostr( DTSS.Second ) ;
            CVO_Minute : Time := '+0:0:' + inttostr( DTSS.Minute ) ;
            CVO_Hundredth : Time := '+0:0:0:0.' + inttostr( DTSS.Billionths div 10000000 ) ;
            CVO_Day, CVO_Date : Time := inttostr( DTSS.Day ) ;
            CVO_Time : Time := '0:'+ LPad( inttostr( DTSS.Hour ), 2, '0' ) + ':' +
                                   LPad( inttostr( DTSS.Minute ), 2, '0' ) + ':' +
                                   LPad( inttostr( DTSS.Second ), 2, '0' ) + '.' +
                                   copy( LPad( inttostr( DTSS.Billionths ), 9, '0' ), 1, 2 ) ;
            CVO_DateTime : Time := inttostr( DTSS.Day ) + ':' +
                                   LPad( inttostr( DTSS.Hour ), 2, '0' ) + ':' +
                                   LPad( inttostr( DTSS.Minute ), 2, '0' ) + ':' +
                                   LPad( inttostr( DTSS.Second ), 2, '0' ) + '.' +
                                   copy( LPad( inttostr( DTSS.Billionths ), 9, '0' ), 1, 2 ) ;
        end ;
    end else
    begin
        Result := LIB_BADTOPT ;
    end ;
end ; // LIB_CVTime
If the requested output format is delta, we generate a delta time, depending on the requested output time. If the requested output format is any other value, we return an error.

    function Get_Absolute( Flags : integer ; var Er : cardinal ) : string ;

    begin
        Len := 0 ;
        setlength( Result, 64 ) ;
        SRB.Buffer := int64( PChar( Result ) ) ;
        SRB.Length := length( Result ) ;
        LIB_SYS_ASCTIM( integer( @Len ), integer( @SRB ), Timestamp, Flags ) ;
        setlength( Result, Len ) ;
    end ;
The local Get_Absolute function converts a timestamp to an ASCII string in the default system time format. We'll discuss LIB_SYS_ASCTIM in a future article.

function LIB_Parse_Date_Time( Context : int64 ; S : string ;
    var DTSS : TDTSS ) : integer ;

var ContextI : TDate_Time_Context ;
    C, I, Loop, O : integer ;
    St, En : integer ;// Starting and ending positions in input string 
    SL : TStringList ;
    N, T, TN : string ;
    Y, Mo, D, H, M, Sec : word ;
    NS : longint ;
    TS : int64 ;

begin
    // Setup...
    Result := 0 ; // Assume success
    S := trim( lowercase( S ) ) ;
    fillchar( DTSS, sizeof( DTSS ), 0 ) ;
    DTSS.Flags := 255 ; // Starting out, everything must be defaulted
LIB_Parse_Date_Time takes a date/time in string format, a DTSS structure, and an optional date/time format instance. We will discuss these instances below. We start out by initializing the DTSS structure - all zeros except for the Flags which have the DTF_* constants necessary to indicate which parts of the date/time are defaulted (we assume they are all defaulted unless we find a given part in the provided string).

    if( Context = 0 ) then
    begin
        ContextI := TDate_Time_Context.Create ;
    end else
    begin
        ContextI := TDate_Time_Context( pointer( Context ) ) ;
    end ;
If a context is 0, we create a date/time context, otherwise we cast the passed integer as an existing date/time context instance.

    // Handle quotes...
    if( copy( S, 1, 1 ) = '"' ) then
    begin
        DTSS.Quoted := True ;
        S := copy( S, 2, length( S ) ) ; // Trim leading quote
        I := pos( '"', S ) ;
        if( I > 0 ) then
        begin
            DTSS.Last_Valid_Position := I ;
            setlength( S, I - 1 ) ;
        end else
        begin
            DTSS.Last_Valid_Position := length( S ) ;
        end ;
    end ;
As described a couple of articles ago, date/time specifications can be enclosed in quotes. So, we check for a date/time string that starts with a quote. If found, we remove it and set the Quoted flag in DTSS. We then look for a closing quote. If one is found, we trim the remainder of the string. In essence, if the date/time is enclosed in quotes, we ignore anything after the quoted portion of the string. However, there cannot be anything else preceeding the initial quote - if the string doesn't start with a quote, it will not be treated as a quoted date/time.

    // Process string...
    St := 1 ;
    En := 0 ;
    SL := ContextI.Parsed_List( LIB_K_INPUT_FORMAT ) ;
    for I := 0 to SL.Count - 1 do
    begin
First we get a parsed list of the input format from the context, and then we loop through the items. We will discuss what this parsed list is later, but for now understand that it is a list of items, each one of which is either a delimiter or a date/time code (starting with an exclamation point).

        if( St > length( S ) ) then
        begin
            break ; // Hit end of input string
        end ;
        if( S[ St ] = '+' ) then // Hit a delta time specification
        begin
            En := St - 1 ;
            DTSS.Last_Valid_Position := En ;
            break ;
        end ;
        T := SL[ I ] ;
        if( I < SL.Count - 1 ) then
        begin
            TN := SL[ I + 1 ] ;
        end else
        begin
            TN := '' ;
        end ;
        if( copy( T, 1, 1 ) <> '!' ) then // A delimiter
        begin
            if( copy( S, St, length( T ) ) <> T ) then
            begin
                if( ( copy( S, St, 1 ) <> ' ' ) or ( T <> ':' ) ) then // Space can be used in place of colon
                begin
                    DTSS.Last_Valid_Position := En ;
                    break ;
                end ;
            end ;
            St := St + length( T ) ;
            En := St ;
        end else
Each time through the loop, we check to make sure we haven't reached the end of the input string. We can reach the end before we've processed all input fields in the parsed list. In that case, it means that the unspecified items should be defaulted, so we exit the loop.

If we encounter a plus sign, we have encountered a delta time specification (either stand-alone or as part of a combination date/time). We Update the ending position to the current position and set the Last_Valid_Position field, then exit the loop.

Now we get the next item in the parsed list, and the following item (if we're at the end of the list, we assume the next item is null). If the item doesn't begin with an exclamation (!) then the item is a delimiter (such as a dash or colon). In the case of a delimiter, if the delimiter doesn't match what's at the current point of the input string, we mark the last valid position and exit the loop. Otherwise we update the St and En values and loop back for the next item. We also allow a space to be used in place of a colon delimiter.

        if( ( TN = '' ) or ( copy( S, 1, length( TN ) ) <> TN ) or ( copy( TN, 1, 1 ) = '!' ) ) then 
        begin// Haven't hit next delimiter
            // Process item...
            if( copy( T, 1, 3 ) = '!MI' ) then // Meridium indicator
            begin
                O := ContextI.Index( LIB_K_MERIDIEM_INDICATOR_L, S, St, En ) ;
                if( O = 0 ) then // Not valid
                begin
                    break ;
                end ;
                DTSS.Flags := DTSS.Flags and ( not DTF_Defaulted_Meridium ) ;
                DTSS.Meridium := O ;
            end else
If there is another parsed item following the curent one and the current position in the input string matches it, and the next item is a code, then what we have is a situation where the input string has omitted an item. In other words, the current position is the next delimiter. In such a case we drop down to the end of the loop and move on to the next item (the delimiter).

But if the foregoing is not the case, we process the current code appropriately. We start with a meridium indicator (any of them starting with "!MI"). Note that there are several meridium indicator codes, but we match on any of them, because we do not require that the input string match the exact case. If the code is a meridium indicator, we find the index in the meridium list for the date/time format. If the index is 0, the supplied data doesn't match a valid meridium indicator and we exit the loop because we've found a place that doesn't match the input specification for date/time. Otherwise, we clear the flag indicating that the meridium is not defaulted and set the DTSS.Meridium value to the index. This is the basic operation of all of the following code handling situations.

            if( copy( T, 1, 3 ) = '!MA' ) then // Month alphabetic
            begin
                if( DTSS.Relative <> 0 ) then // Already have a relative date specified
                begin
                    Skip_Date_Delimiter( S, St, En ) ;
                    St := En ;
                    continue ;
                end ;
                O := ContextI.Index( LIB_K_RELATIVE_DAY_NAME_L, S, St, En ) ;
                if( O > 0 ) then
                begin
                    Skip_Date_Delimiter( S, St, En ) ;
                    DTSS.Relative := O ;
                    St := En ;
                    continue ;
                end ;
                O := ContextI.Index( LIB_K_MONTH_NAME_L, S, St, En ) ;
                if( O = 0 ) then
                begin
                    O := ContextI.Index( LIB_K_MONTH_NAME_ABB_L, S, St, En ) ;
                    if( O = 0 ) then
                    begin
                        break ;
                    end ;
                end ;
                DTSS.Month := O ;
                DTSS.Flags := DTSS.Flags and ( not DTF_Defaulted_Month ) ;
            end else
The alphabetic month code is handled much the same way as the meridium indicator code, but there is additional processing for date codes. It is possible that some previous loop processed a relative day specification (e.g. "yesterday" or "tomorrow"). If that has been done, then the Relative value will have been set in DTSS. In that case, we use Skip_Date_Delimiter to skip past this item, and continue with the next iteration of the loop. Otherwise, we check to see if the current input string position is a relative day. We do this by finding the index in the relative day name list. If found, the Relative value is set for future date code processing, the delimiter is skipped, and the current position is updated, then we continue with the next loop iteration. But if we aren't dealing with a relative day, we look up the month name index. We check both the full name and the abbreviated name for a match. As before, if there is no match, we exit the loop. Otherwise we set the flags and Month field in DTSS.

            if( copy( T, 1, 3 ) = '!MN' ) then // Month numeric
            begin
                if( DTSS.Relative <> 0 ) then // Already have a relative date specified
                begin
                    Skip_Date_Delimiter( S, St, En ) ;
                    St := En ;
                    continue ;
                end ;
                O := ContextI.Index( LIB_K_RELATIVE_DAY_NAME_L, S, St, En ) ;
                if( O > 0 ) then
                begin
                    Skip_Date_Delimiter( S, St, En ) ;
                    DTSS.Relative := O ;
                    St := En ;
                    continue ;
                end ;
                N := Parse_Number( S, St, En ) ;
                if( N = '' ) then
                begin
                    break ;
                end ;
                O := strtoint( N ) ;
                if( ( O < 1 ) or ( O > 12 ) ) then
                begin
                    En := St ;
                    break ;
                end ;
                DTSS.Month := O ;
                DTSS.Flags := DTSS.Flags and ( not DTF_Defaulted_Month ) ;
            end else
The processing for the numeric month is nearly identical to the month name processing. Except, instead of looking up the month name, we use Parse_Number to validate and obtain the current number. We validate the month number is between 1 and 12, inclusive. No matter what localization we are dealing with, all cases use a 12-month year.

            if( copy( T, 1, 2 ) = '!W' ) then // Weekday
            begin
                if( DTSS.Relative <> 0 ) then // Already have a relative date specified
                begin
                    Skip_Date_Delimiter( S, St, En ) ;
                    St := En ;
                    continue ;
                end ;
                O := ContextI.Index( LIB_K_RELATIVE_DAY_NAME_L, S, St, En ) ;
                if( O > 0 ) then
                begin
                    Skip_Date_Delimiter( S, St, En ) ;
                    DTSS.Relative := O ;
                    St := En ;
                    continue ;
                end ;
                O := ContextI.Index( LIB_K_WEEKDAY_NAME_L, S, St, En ) ;
                if( O = 0 ) then
                begin
                    O := ContextI.Index( LIB_K_WEEKDAY_NAME_ABB_L, S, St, En ) ;
                    if( O = 0 ) then
                    begin
                        break ;
                    end ;
                end ;
                DTSS.Weekday := O ;
            end else
Weekdays are handled much the same as month names. However, I should point out that input formats containing weekday codes are logically questionable. If a day is specified, a weekday is redundant, at best. If a day isn't specified, a weekday is ambiguous. And what do you do if the user enters a weekday that differs from what the specified day of the month actually is? Weekday codes are primarily used in date output formatting and should be avoided in input format specifications. Nevertheless, we will process one if it is included, and verify that the value matches a valid weekday name - but we don't check consistency with actual dates.

            if( copy( T, 1, 2 ) = '!D' ) then // Day
            begin
                if( DTSS.Relative <> 0 ) then // Already have a relative date specified
                begin
                    Skip_Date_Delimiter( S, St, En ) ;
                    St := En ;
                    continue ;
                end ;
                O := ContextI.Index( LIB_K_RELATIVE_DAY_NAME_L, S, St, En ) ;
                if( O > 0 ) then
                begin
                    Skip_Date_Delimiter( S, St, En ) ;
                    DTSS.Relative := O ;
                    St := En ;
                    continue ;
                end ;
                N := Parse_Number( S, St, En ) ;
                if( N = '' ) then
                begin
                    break ;
                end ;
                O := strtoint( N ) ;
                if( ( O < 1 ) or ( O > 31 ) ) then
                begin
                    En := St ;
                    break ;
                end ;
                DTSS.Flags := DTSS.Flags and ( not DTF_Defaulted_Day ) ;
                DTSS.Day := O ;
            end else
            if( copy( T, 1, 2 ) = '!H' ) then // Hour
            begin
                N := Parse_Number( S, St, En ) ;
                if( N = '' ) then
                begin
                    break ;
                end ;
                O := strtoint( N ) ;
                if( ( O < 0 ) or ( O > 23 ) ) then
                begin
                    En := St ;
                    break ;
                end ;
                DTSS.Hour := O ;
                DTSS.Flags := DTSS.Flags and ( not DTF_Defaulted_Hour ) ;
            end else
            if( copy( T, 1, 2 ) = '!S' ) then // Second
            begin
                N := Parse_Number( S, St, En ) ;
                if( N = '' ) then
                begin
                    break ;
                end ;
                O := strtoint( N ) ;
                if( ( O < 0 ) or ( O > 59 ) ) then
                begin
                    En := St ;
                    break ;
                end ;
                DTSS.Second := O ;
                DTSS.Flags := DTSS.Flags and ( not DTF_Defaulted_Second ) ;
            end else
            if( copy( T, 1, 2 ) = '!M' ) then // Minute
            begin
                N := Parse_Number( S, St, En ) ;
                if( N = '' ) then
                begin
                    break ;
                end ;
                O := strtoint( N ) ;
                if( ( O < 0 ) or ( O > 59 ) ) then
                begin
                    En := St ;
                    break ;
                end ;
                DTSS.Minute := O ;
                DTSS.Flags := DTSS.Flags and ( not DTF_Defaulted_Minute ) ;
            end else
            if( ( copy( T, 1, 2 ) = '!Y' ) or ( copy( T, 1, 2 ) = '!Z' ) ) then // Year
            begin
                if( DTSS.Relative <> 0 ) then // Already have a relative date specified
                begin
                    Skip_Date_Delimiter( S, St, En ) ;
                    St := En ;
                    continue ;
                end ;
                O := ContextI.Index( LIB_K_RELATIVE_DAY_NAME_L, S, St, En ) ;
                if( O > 0 ) then
                begin
                    Skip_Date_Delimiter( S, St, En ) ;
                    DTSS.Relative := O ;
                    St := En ;
                    continue ;
                end ;
                N := Parse_Number( S, St, En ) ;
                if( N = '' ) then
                begin
                    break ;
                end ;
                O := strtoint( N ) ;
                DTSS.Year := O ;
                DTSS.Flags := DTSS.Flags and ( not DTF_Defaulted_Year ) ;
            end else
Most of the remaining codes are handled like the first two we discussed so we won't go into more detail about them here.

            if( copy( T, 1, 2 ) = '!C' ) then // Fractional seconds
            begin
                N := Parse_Number( S, St, En ) ;
                if( N = '' ) then
                begin
                    break ;
                end ;
                C := 0 ;
                for Loop := 1 to length( N ) do
                begin
                    if( N[ Loop ] = '0' ) then // Leading zero
                    begin
                        inc( C ) ;
                    end else
                    begin
                        break ;
                    end ;
                end ;
                O := strtoint( N ) ;
                for Loop := length( N ) to 9 do
                begin
                    O := O * 10 ;
                end ;
                while( C > 0 ) do
                begin
                    dec( C ) ;
                    O := O div 10 ;
                end ;
                DTSS.Billionths := O ;
                DTSS.Flags := DTSS.Flags and ( not DTF_Defaulted_Tenths ) ;
            end else
            begin
                DTSS.Flags := DTSS.Flags or DTF_Error ; // Invalid item
                break ;
            end ;
            St := En + 1 ;
        end ;
    end ; // for I := 0 to Parsed_Input.Count - 1
The final code we handle is for fractional seconds. First we get the specified number with Parse_Number. Then we count up the number of leading zeros in the number. Then we convert to billionths, which is what UOS time counts in. Note that the input could have any number of fractional digits, so we multiple the value by 10 a number of times equal to the difference between 9 digits (1 billionth) and the actual number of digits specified. Then we divide by 10 for the number of leading zeros. Then we set the flags and Billionths field.

If the code is not recognized, we set an error and exit the loop. Otherwise, we adjust the input string position and loop back for the next item.

    // Default the unspecified date parts...
    Parse_Sirius_Timestamp( LIB_Get_Timestamp, Y, Mo, D, H, M, Sec, NS ) ;
    if( DTSS.Relative <> 0 ) then
    begin
        case DTSS.Relative of
            1 : // Yesterday
                begin
                    TS := Encode_Sirius_Timestamp( Y, Mo, D - 1, H, M, Sec, NS ) ;
                    Parse_Sirius_Timestamp( TS, Y, Mo, D, H, M, Sec, NS ) ;
                end ;
            3 : // Tomorrow
                begin
                    D := D + 1 ;
                    Parse_Sirius_Timestamp( TS, Y, Mo, D, H, M, Sec, NS ) ;
                end ;
        end ;
        DTSS.Year := Y ;
        DTSS.Month := Mo ;
        DTSS.Day := D ;
    end else
    begin
        if( ( DTSS.Flags and DTF_Defaulted_Day ) <> 0 ) then
        begin
            DTSS.Day := D ;
        end ;
        if( ( DTSS.Flags and DTF_Defaulted_Year ) <> 0 ) then
        begin
            DTSS.Year := Y ;
        end ;
        if( ( DTSS.Flags and DTF_Defaulted_Month ) <> 0 ) then
        begin
            DTSS.Month := Mo ;
        end ;
    end ;
Once we've processed through the input date/time string, we default the omitted parts. First, we parse the current time into the local variables for dates and times. If a relative day was specified, we get a timestamp for the previous or next day, as appropriate. Note that we use the current date/time values other than the day. Then we parse that timestamp and update the year, month, and day from the new timestamp values. We don't update the time in DTSS since that isn't affected by relative days. We do update the year and month though, since the previous or next day may be in a different month and/or year.

If there was no relative day specified, we update DTSS with the current day, year, and month, if any of those parts were omitted.

    // Default the unspecified time parts...
    if( ( DTSS.Flags and DTF_Defaulted_Hour ) <> 0 ) then
    begin
        DTSS.Hour := H ;
    end ;
    if( ( DTSS.Flags and DTF_Defaulted_Minute ) <> 0 ) then
    begin
        DTSS.Minute := M ;
    end ;
    if( ( DTSS.Flags and DTF_Defaulted_Second ) <> 0 ) then
    begin
        DTSS.Second := Sec ;
    end ;
    if( ( DTSS.Flags and DTF_Defaulted_Tenths ) <> 0 ) then
    begin
        DTSS.Billionths := NS ;
    end ;
Next we default the various time fields in DTSS that were omitted, with the current time.

    // Check for delta time...
    St := En + 1 ;
    if( ( copy( S, St, 1 ) = '+' ) or ( copy( S, St, 1) = '-' ) ) then
    begin
        if( S[ St ] = '+' ) then
        begin
            inc( St ) ;
            inc( En ) ;
        end ;
        if( copy( S, St, 1 ) = '-' ) then
        begin
            DTSS.Flags := DTSS.Flags or DTF_Negative ;
            inc( St ) ;
            inc( En ) ;
        end ;
Now we are ready to handle delta time specifications. If the next character of the input time is a plus or minus, we have a delta time. In that case, if the character is a plus, we skip over it. Then we check for a minus and skip over it if found. This handles both the "+" and "+-" constructs.

        DTSS.Delta_Days := Parse_integer ;
        St := En ;
        if( copy( S, En + 1, 1 ) = ':' ) then // Delta time is included
        begin
            St := En + 2 ;
            DTSS.Delta_Hours := Parse_Integer ;
            St := En ;
            if( copy( S, En + 1, 1 ) = ':' ) then // Delta minutes is included
            begin
                St := En + 2 ;
                DTSS.Delta_Minutes := Parse_Integer ;
                St := En ;
                if( copy( S, En + 1, 1 ) = ':' ) then // Delta seconds is included
                begin
                    St := En + 2 ;
                    DTSS.Delta_Seconds := Parse_Integer ;
                end ;
                St := En ;
We get the day portion of the delta. If a colon follows, we skip over it and get the delta hours value. We repeat this process with the minutes and seconds.

                if( copy( S, En + 1, 1 ) = '.' ) then // Delta billionths is included
                begin
                    C := 0 ;
                    for Loop := St to En do
                    begin
                        if( S[ Loop ] = '0' ) then // Leading zero
                        begin
                            inc( C ) ;
                        end else
                        begin
                            break ;
                        end ;
                    end ;
                    DTSS.Delta_Billionths := Parse_Integer ;
                    for Loop := En - St to 8 do
                    begin
                        DTSS.Delta_Billionths := DTSS.Delta_Billionths * 10 ;
                    end ;
                    while( C > 0 ) do
                    begin
                        DTSS.Delta_Billionths := DTSS.Delta_Billionths div 10 ;
                    end ;
                    St := En ;
                end ; // if( copy( S, En + 1, 1 ) = '.' )
            end ; // if( copy( S, En + 1, 1 ) = ':' )
        end ; // if( copy( S, En + 1, 1 ) = ':' )
    end ; // Delta time
If a decimal point is encountered, we know that we have a fractional second. As above, we need to convert this to billionths. So we count the number of leading zeroes. Then we get the numeric value, repeatedly multiply by 10 to convert to 9 digits, then divide by 10 according the number of leading zeroes. By the way, we always multiple first, then divide second. Since the division is integer, we would potentially lose significant digits if we divided first and multiplied second.

    DTSS.Last_Valid_Position := En ;

    // Cleanup...
    if( Context = 0 ) then
    begin
        ContextI.Free ;
    end ;
end ; // LIB_Parse_Date_Time
Finally, we update the last valid position in DTSS with the last processed position in the input string. This allows the calling code to know where/if the input date/time was invalid. If the last valid position is also the last character of the input then the whole thing was valid. Lastly, if we created a date/time context instance, we free it.

function Parse_Number( const S : string ; St : integer ; var En : integer ) : string ;

begin
    // Trim leading spaces...
    while( St <= length( S ) ) do
    begin
        if( S[ St ] <> ' ' ) then
        begin
            break ;
        end ;
        inc( St ) ;
    end ;
    En := St ;

    // Parse number...
    while( En <= length( S ) ) do
    begin
        if( pos( S[ En ], '0123456789' ) = 0 ) then
        begin
            break ;
        end ;
        inc( En ) ;
    end ;

    // Adjust end position...
    if( ( En > length( S ) ) or ( pos( S[ En ], '0123456789' ) = 0 ) ) then
    begin
        dec( En ) ;
    end ;

    // Return value...
    Result := copy( S, St, En - St + 1 ) ;
end ; // Parse_Number
The Parse_Number function is used to get an integer number from a string, starting at position St. The function returns the numeric value (or 0 if none found) and the ending position of the found number.

We skip past blanks, which are ignored. Then we process the number as long as consecutive digits are found. Embedded spaces are not allowed - encountering a space after the leading spaces (if any) ends the parsing. Finally, we update the ending position and return the number.

In the next article, we will discuss the date/time context class that we used in the above code.

 

Copyright © 2020 by Alan Conroy. This article may be copied in whole or in part as long as this copyright is included.