# HG changeset patch # User Claus Gittinger # Date 1359120875 -3600 # Node ID eb944b3267e6d3c01e46b00b1f8b5d0ba9e3b96c # Parent d8b5f5e6505986f3c9ebf4ee18d6e8cdfe486d43 class: Timestamp added: #readFrom:format:language:onError: #utcOffsetFrom: changed: #readRFC1123FormatFrom:onError: fixed utcOffset handling diff -r d8b5f5e65059 -r eb944b3267e6 Timestamp.st --- 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!