Timestamp.st
changeset 14713 eb944b3267e6
parent 14428 cb61ee2ba157
child 14969 13470ec6f8c8
child 18017 7fef9e17913f
--- a/Timestamp.st	Fri Jan 25 11:37:50 2013 +0100
+++ b/Timestamp.st	Fri Jan 25 14:34:35 2013 +0100
@@ -618,6 +618,182 @@
 
 !Timestamp class methodsFor:'reading'!
 
+readFrom:aStringOrStream format:formatString language:languageOrNil onError:exceptionalValue
+    "return a new Timestamp, reading a printed representation from aStream using a formatString.
+     The formatString is similar to the one used when printing.
+     On error, exceptionalValue is returned. If exceptionalValue is a one-arg block, an error message is
+     passed as argument.
+     format:
+        %h      hours, 00..23 (i.e. european)  0-padded to length 2
+        %u      hours, 00..12 (i.e. us)        0-padded to length 2
+        %m      minutes, 00..59                0-padded to length 2
+        %s      seconds, 00..59                0-padded to length 2
+        %i      milliseconds, 000..999         0-padded to length 3
+        %a      am/pm
+
+        %d             - day          
+        %D             - day          
+        %(day)         - day          
+
+        %m             - month                
+        %M             - month                
+        %(month)       - month                
+
+        %(monthName)   - monthName                
+
+        %(year)        - year, full 4 digits
+        %Y             - year, last 2 digits only, 
+                         0..71 map to 2000..2071; 
+                         72..99 map to 1972..1999; 
+        %Y1900          - year, last 2 digits only, map to 1900..1999 
+        %Y2000          - year, last 2 digits only, map to 2000..2099 
+
+    an optional length after the % gives a field length;
+        i.e. %2h%2m%2s parses 123557 as 12:35:37
+"
+
+    |day month year
+     hour minute second millisecond
+     utcOffset inStream formatStream error fChar format itemHandler 
+     len now s|
+
+    error := [:msg | 
+                exceptionalValue isBlock ifTrue:[
+                    ^ exceptionalValue valueWithOptionalArgument:'format error; space expcected'
+                ] ifFalse:[
+                    ^ exceptionalValue value
+                ].
+             ].
+
+    itemHandler := [:format |
+        |input|
+        input := len isNil ifTrue:[ inStream ] ifFalse:[ inStream next: len ].
+
+        ( #('d' 'D' 'day' ) includes:format ) ifTrue:[
+            day := Integer readFrom:input onError:[ error value:'invalid day' ].
+
+        ] ifFalse:[ ( format = 'month' ) ifTrue:[
+            month := Integer readFrom:input onError:[ error value:'invalid month' ].
+
+        ] ifFalse:[ ( format = 'year' or:[ format = 'y' ]) ifTrue:[
+            year := Integer readFrom:input onError:[ error value:'invalid year' ].
+
+        ] ifFalse:[ ( format = 'Y' ) ifTrue:[
+            year := Integer readFrom:input onError:[ error value:'invalid year' ].
+            (year between:0 and: 99) ifFalse:[ error value:'invalid year' ].
+            (year between:0 and:71) ifTrue:[
+                year := year + 1900
+            ] ifFalse:[
+                year := year + 2000
+            ]
+
+        ] ifFalse:[ (format = 'monthName') ifTrue:[
+            s := input nextMatching:[:c | c isLetter] thenMatching:[:c | c isLetter].
+            month := Date indexOfMonth:s asLowercase language:languageOrNil
+
+        ] ifFalse:[ ( format = 'Y1900' ) ifTrue:[
+            year := Integer readFrom:input onError:[ error value:'invalid year' ].
+            (year between:0 and: 99) ifFalse:[ error value:'invalid year' ].
+            year := year + 1900
+
+        ] ifFalse:[ ( format = 'Y2000' ) ifTrue:[
+            year := Integer readFrom:input onError:[ error value:'invalid year' ].
+            (year between:0 and: 99) ifFalse:[ error value:'invalid year' ].
+            year := year + 2000
+
+        ] ifFalse:[ ( format = 'h' or:[ format = 'H' ]) ifTrue:[
+            hour := Integer readFrom:input onError:[ error value:'invalid hour' ].
+
+        ] ifFalse:[ ( format = 'u'  or:[ format = 'U']) ifTrue:[
+            hour := Integer readFrom:input onError:[ error value:'invalid hour' ].
+
+        ] ifFalse:[ ( format = 'm'  or:[ format = 'M' ]) ifTrue:[
+            minute := Integer readFrom:input onError:[ error value:'invalid minute' ].
+
+        ] ifFalse:[ ( format = 's'  or:[ format = 'S' ]) ifTrue:[
+            second := Integer readFrom:input onError:[ error value:'invalid second' ].
+
+        ] ifFalse:[ ( format = 'i'  or:[ format = 'I' ]) ifTrue:[
+            millisecond := Integer readFrom:input onError:[ error value:'invalid month' ].
+
+        ] ifFalse:[ ( format = 'tz' ) ifTrue:[
+            utcOffset := self utcOffsetFrom:input.
+            utcOffset isNil ifTrue:[ error value:'invalid timezone' ]
+
+        ] ifFalse:[ ( format = 'a' ) ifTrue:[
+            s := (input next:2) asLowercase.
+            s = 'am' ifTrue:[
+                (hour between:0 and:12) ifFalse:[ error value:'invalid hour' ]
+            ] ifFalse:[
+                s = 'pm' ifTrue:[
+                    (hour between:1 and:12) ifFalse:[ error value:'invalid hour' ].
+                    hour := hour + 12.
+                ] ifFalse:[
+                    error value:'invalid am/pm' 
+                ]
+            ]
+
+        ] ifFalse:[
+            error value:'unhandled format:',format
+        ]]]]]]]]]]]]]]
+   ].
+
+    hour := 0.
+    minute := 0.
+    second := 0.
+    millisecond := 0.
+
+    inStream := aStringOrStream readStream.
+    formatStream := formatString readStream.
+
+    [formatStream atEnd] whileFalse:[
+        fChar := formatStream next.
+        fChar = Character space ifTrue:[
+            inStream peek isSeparator ifFalse:[ error value: 'format error; space expcected' ].
+            inStream skipSeparators.
+        ] ifFalse:[
+            fChar == $% ifTrue:[
+                len := nil.
+                (formatStream peek isDigit) ifTrue:[
+                    len := Integer readFrom:formatStream onError:[ error value: 'format error; invalid length' ]
+                ].
+                (formatStream peek == $() ifTrue:[
+                    formatStream next.
+                    format := formatStream upTo:$).
+                ] ifFalse:[
+                    (formatStream peek == ${) ifTrue:[
+                        formatStream next.
+                        format := formatStream upTo:$}.
+                    ] ifFalse:[
+                        (formatStream peek isLetter) ifTrue:[
+                            format := formatStream nextAlphaNumericWord.
+                        ] ifFalse:[
+                            error value:'unhandled format:',formatStream peek
+                        ]
+                    ]
+                ].
+                itemHandler value:format.
+            ] ifFalse:[
+                inStream peek = fChar ifFalse:[^ error value: 'format error; ',fChar,' expcected'].
+                inStream next.
+            ]
+        ].
+    ].
+
+    year isNil ifTrue:[
+        year := (now := Timestamp now) year
+    ].
+
+    ^ (self year:year month:month day:day hour:(hour ? 0) minute:(minute ? 0) second:(second ? 0) millisecond:millisecond) - utcOffset
+
+    "
+     Timestamp readFrom:'20-2-1995 13:11:06' format:'%day-%month-%year %h:%m:%s' language:nil onError:[self halt]   
+     Timestamp readFrom:'20021995131106' format:'%2d%2month%4y%2h%2m%2s' language:nil onError:[self halt]   
+     Timestamp readFrom:'March 7 2009 7:30pm EST' format:'%monthName %day %year %u:%m%a %tz' language:#en onError:[self halt]
+     Timestamp readFrom:'March 7 2009 7:30pm UTC' format:'%monthName %day %year %u:%m%a %tz' language:#en onError:[self halt]  
+    "
+!
+
 readFrom:aStringOrStream onError:exceptionBlock
     "return a new Timestamp, reading a printed representation from aStream.
      The string is interpreted as 24 hour format, as printed.
@@ -919,8 +1095,6 @@
 !
 
 readRFC1123FormatFrom:rfc1123String onError:exceptionBlock
-    |parts indexModifier utcOffsetString utcOffset day year time monthName month|
-
 "/    All HTTP/1.0 date/time stamps must be represented in Universal Time (UT), 
 "/    also known as Greenwich Mean Time (GMT), without exception. 
 "/    This is indicated in the first two formats by the inclusion of "GMT" as the three-letter abbreviation for time zone, 
@@ -953,6 +1127,15 @@
 "/                   | "Sep" | "Oct" | "Nov" | "Dec"
 "/
 "/    Mon, 17 Aug 2009 11:11:15 GMT
+"/
+"/ however, occasionally, someone presents us with non-UTC strings which include a timezone;
+"/ thus, this also supports:
+"/    Mon, 17 Aug 2009 11:11:15 +xxxx
+"/    Mon, 17 Aug 2009 11:11:15 -xxxx
+"/ and:
+"/    Mon, 17 Aug 2009 11:11:15 PST
+
+    |parts indexModifier utcOffsetString utcOffset day year time monthName month|
 
     rfc1123String isEmptyOrNil ifTrue:[^ exceptionBlock value].
 
@@ -967,18 +1150,8 @@
         ].
     ].
 
-    utcOffset := 0.
     utcOffsetString := (parts at:6 + indexModifier).
-    ((utcOffsetString sameAs:'GMT') or:[utcOffsetString sameAs:'UTC']) ifFalse:[ 
-        self assert:utcOffsetString size == 5.
-
-        utcOffset := (utcOffsetString from:4 to:5) asString asNumber * 60.
-        utcOffset := utcOffset + ((utcOffsetString from:2 to:3) asString asNumber * 60 * 60).
-
-        (utcOffsetString at:1) asSymbol == #- ifTrue:[
-            utcOffset := -1 * utcOffset.
-        ].
-    ].
+    utcOffset := (self utcOffsetFrom:utcOffsetString) ? 0.
 
     day := Integer readFrom:(parts at:2 + indexModifier) onError:[^ exceptionBlock].
     year := Integer readFrom:(parts at:4 + indexModifier) onError:[^ exceptionBlock].
@@ -991,9 +1164,307 @@
 
     ^ (self 
         fromDate:(Date newDay:day monthIndex:month year:year) 
-        andTime:time) + utcOffset
+        andTime:time) - utcOffset
+
+    "
+     self readRFC1123FormatFrom:'Mon, 17 Aug 2009 11:11:15 PST' onError:nil        
+     self readRFC1123FormatFrom:'Mon, 17 Aug 2009 11:11:15 PDT' onError:nil        
+     self readRFC1123FormatFrom:'Mon, 17 Aug 2009 11:11:15 UTC' onError:nil        
+     self readRFC1123FormatFrom:'Mon, 17 Aug 2009 11:11:15 +0100' onError:nil        
+    "
 
     "Modified: / 05-10-2010 / 16:05:32 / cg"
+!
+
+utcOffsetFrom:aStringOrStream
+    "return the utcOffset (in seconds) for a given time-zone name.
+     Returns nil for invalid formats"
+
+    |table i offset stream tzName sign|
+
+    table :=
+        #(
+            'GMT'   0                  
+            'UTC'   0                 
+
+            "/ US
+            'AST'   -4                 "/ atlantic
+            'ADT'   -3                  
+
+            'AKST'   -8                "/ alaska
+            'AKDT'   -9      
+
+            'CST'   -6                 "/ central
+            'CDT'   -5      
+
+            'EST'   -5                 "/ eastern
+            'EDT'   -4      
+
+            'EGST'   0                 "/ east greenland
+            'EGT'   -1      
+
+            'HADT'   -9                "/ hawaii
+            'HAST'   -10      
+
+            'MST'   -7                 "/ mountain
+            'MDT'   -6      
+
+            'NST'   -3.5                 "/ new foundland
+            'NDT'   -2.5      
+
+            'PST'   -8                 "/ pacific
+            'PDT'   -7      
+
+            'PMST'  -3                 "/ pierre & miquelon
+            'PMDT'  -2      
+
+            'WGST'  -2                 "/ west greenland
+            'WGT'   -3      
+
+            "/ europe
+            'CET'   1                 "/ central european
+            'CEST'  2                
+
+            'EET'   2                 "/ east european
+            'EEST'  3              
+
+            'WET'   0                  "/ west european
+            'WEST'  1                 
+
+            'MSK'   4                  "/ moscow european
+            'MSD'   4         
+
+            "/ pacific
+            'NZST' 12                 "/ new zealand
+            'NZDT' 13                
+
+            "/ south america
+            'ART'   -3                 "/ argentina
+            'BOT'   -4                 "/ bolivia
+            'BRT'   -3                 "/ brasilia
+            'BRST'  -2                 
+            'CLT'   -4                 "/ chile
+            'CLST'  -3                  
+            'ECT'   -5                 "/ equador
+            'PET'   -5                 "/ peru
+            'PYT'   -4                 "/ paraguay
+            'UYT'   -3                 "/ uruguay
+            'VET'   -4.5               "/ venezuela
+
+            "/ africa
+            'CAT'   2                 "/ central africa
+            'EAT'   3                 "/ east africa
+            'SAST'  2                 "/ south africa
+            'WAT'   1                 "/ west africa
+            'WAST'  2                
+            'WT'    0                  "/ west sahara
+            'WST'   1                   
+
+            'HKT'   8                 "/ hongkong           
+            'IST'   5.5                "/ india           
+            'JST'   9                 "/ japan           
+            'KST'   9                 "/ korea           
+            'SGT'   8                 "/ singapore
+
+            "/ military
+            'A'     1                   
+            'B'     2                   
+            'C'     3                   
+            'D'     4                   
+            'E'     5                   
+            'F'     6                   
+            'G'     7                   
+            'H'     8                   
+            'I'     9                   
+            'K'     10                   
+            'L'     11                  
+            'M'     12                  
+            'N'     -1                   
+            'O'     -2                   
+            'P'     -3                   
+            'Q'     -4                   
+            'R'     -5                   
+            'S'     -6                   
+            'T'     -7                   
+            'U'     -8                   
+            'V'     -9                   
+            'W'     -10                  
+            'X'     -11                  
+            'Y'     -12                  
+        ).
+
+    stream := aStringOrStream readStream.
+    stream skipSeparators.
+
+    stream peek isLetter ifTrue:[
+        tzName := stream upToMatching:[:ch | ch isLetter not].
+
+        i := table indexOf:tzName.
+        i ~~ 0 ifTrue:[ 
+            ^ (table at:i+1) * 60 * 60 
+        ].
+    ] ifFalse:[
+        sign := 1.
+        stream peek == $- ifTrue:[
+            sign := -1.
+            stream next.
+        ] ifFalse:[
+            stream peek == $+ ifTrue:[
+                sign := 1.
+                stream next.
+            ] ifFalse:[
+                stream skipSeparators
+            ]
+        ].
+        offset := ((stream next:2) asNumber * 60 * 60).
+        offset := offset + ((stream next:2) asNumber * 60).
+        ^  offset * sign
+    ].
+
+    ^ nil
+
+    "
+     self utcOffsetFrom:'UTC'  
+     self utcOffsetFrom:'PST'  
+     self utcOffsetFrom:'EST'
+     self utcOffsetFrom:'+0130'  
+    "
+!
+
+utcOffsetFromString:aString
+    "return the utcOffset (in seconds) for a given time-zone name.
+     Returns nil for invalid formats"
+
+    |table i offset|
+
+    table :=
+        #(
+            'GMT'   0                  
+            'UTC'   0                 
+
+            "/ US
+            'AST'   -4                 "/ atlantic
+            'ADT'   -3                  
+
+            'AKST'   -8                "/ alaska
+            'AKDT'   -9      
+
+            'CST'   -6                 "/ central
+            'CDT'   -5      
+
+            'EST'   -5                 "/ eastern
+            'EDT'   -4      
+
+            'EGST'   0                 "/ east greenland
+            'EGT'   -1      
+
+            'HADT'   -9                "/ hawaii
+            'HAST'   -10      
+
+            'MST'   -7                 "/ mountain
+            'MDT'   -6      
+
+            'NST'   -3.5                 "/ new foundland
+            'NDT'   -2.5      
+
+            'PST'   -8                 "/ pacific
+            'PDT'   -7      
+
+            'PMST'  -3                 "/ pierre & miquelon
+            'PMDT'  -2      
+
+            'WGST'  -2                 "/ west greenland
+            'WGT'   -3      
+
+            "/ europe
+            'CET'   1                 "/ central european
+            'CEST'  2                
+
+            'EET'   2                 "/ east european
+            'EEST'  3              
+
+            'WET'   0                  "/ west european
+            'WEST'  1                 
+
+            'MSK'   4                  "/ moscow european
+            'MSD'   4         
+
+            "/ pacific
+            'NZST' 12                 "/ new zealand
+            'NZDT' 13                
+
+            "/ south america
+            'ART'   -3                 "/ argentina
+            'BOT'   -4                 "/ bolivia
+            'BRT'   -3                 "/ brasilia
+            'BRST'  -2                 
+            'CLT'   -4                 "/ chile
+            'CLST'  -3                  
+            'ECT'   -5                 "/ equador
+            'PET'   -5                 "/ peru
+            'PYT'   -4                 "/ paraguay
+            'UYT'   -3                 "/ uruguay
+            'VET'   -4.5               "/ venezuela
+
+            "/ africa
+            'CAT'   2                 "/ central africa
+            'EAT'   3                 "/ east africa
+            'SAST'  2                 "/ south africa
+            'WAT'   1                 "/ west africa
+            'WAST'  2                
+            'WT'    0                  "/ west sahara
+            'WST'   1                   
+
+            'HKT'   8                 "/ hongkong           
+            'IST'   5.5                "/ india           
+            'JST'   9                 "/ japan           
+            'KST'   9                 "/ korea           
+            'SGT'   8                 "/ singapore
+
+            "/ military
+            'A'     1                   
+            'B'     2                   
+            'C'     3                   
+            'D'     4                   
+            'E'     5                   
+            'F'     6                   
+            'G'     7                   
+            'H'     8                   
+            'I'     9                   
+            'K'     10                   
+            'L'     11                  
+            'M'     12                  
+            'N'     -1                   
+            'O'     -2                   
+            'P'     -3                   
+            'Q'     -4                   
+            'R'     -5                   
+            'S'     -6                   
+            'T'     -7                   
+            'U'     -8                   
+            'V'     -9                   
+            'W'     -10                  
+            'X'     -11                  
+            'Y'     -12                  
+        ).
+
+    i := table indexOf:aString.
+    i ~~ 0 ifTrue:[ ^ (table at:i+1) * 60 * 60 ].
+
+    aString size == 5 ifTrue:[
+        offset := (aString copyFrom:4 to:5) asNumber * 60.
+        offset := offset + ((offset copyFrom:2 to:3) asNumber * 60 * 60).
+        (aString at:1) == $- ifTrue:[
+            offset := offset negated.
+        ].
+        ^  offset
+    ].
+
+    ^ nil
+
+    "
+     self utcOffsetFrom:'UTC' 
+    "
 ! !
 
 !Timestamp methodsFor:'accessing'!
@@ -2699,11 +3170,12 @@
 !Timestamp class methodsFor:'documentation'!
 
 version
-    ^ '$Header: /cvs/stx/stx/libbasic/Timestamp.st,v 1.142 2012-10-24 22:42:11 cg Exp $'
+    ^ '$Header: /cvs/stx/stx/libbasic/Timestamp.st,v 1.143 2013-01-25 13:34:35 cg Exp $'
 !
 
 version_CVS
-    ^ '$Header: /cvs/stx/stx/libbasic/Timestamp.st,v 1.142 2012-10-24 22:42:11 cg Exp $'
+    ^ '$Header: /cvs/stx/stx/libbasic/Timestamp.st,v 1.143 2013-01-25 13:34:35 cg Exp $'
 ! !
 
+
 Timestamp initialize!