taler-mailbox

Service for asynchronous wallet-to-wallet payment messages
Log | Files | Refs | Submodules | README | LICENSE

pqtime.go (4828B)


      1 package pqtime
      2 
      3 import (
      4 	"errors"
      5 	"fmt"
      6 	"math"
      7 	"strconv"
      8 	"strings"
      9 	"time"
     10 )
     11 
     12 var errInvalidTimestamp = errors.New("invalid timestamp")
     13 
     14 type timestampParser struct {
     15 	err error
     16 }
     17 
     18 func (p *timestampParser) expect(str string, char byte, pos int) {
     19 	if p.err != nil {
     20 		return
     21 	}
     22 	if pos+1 > len(str) {
     23 		p.err = errInvalidTimestamp
     24 		return
     25 	}
     26 	if c := str[pos]; c != char && p.err == nil {
     27 		p.err = fmt.Errorf("expected '%v' at position %v; got '%v'", char, pos, c)
     28 	}
     29 }
     30 
     31 func (p *timestampParser) mustAtoi(str string, begin int, end int) int {
     32 	if p.err != nil {
     33 		return 0
     34 	}
     35 	if begin < 0 || end < 0 || begin > end || end > len(str) {
     36 		p.err = errInvalidTimestamp
     37 		return 0
     38 	}
     39 	result, err := strconv.Atoi(str[begin:end])
     40 	if err != nil {
     41 		if p.err == nil {
     42 			p.err = fmt.Errorf("expected number; got '%v'", str)
     43 		}
     44 		return 0
     45 	}
     46 	return result
     47 }
     48 
     49 func Parse(currentLocation *time.Location, str string) (time.Time, error) {
     50 	p := timestampParser{}
     51 
     52 	monSep := strings.IndexRune(str, '-')
     53 	// this is Gregorian year, not ISO Year
     54 	// In Gregorian system, the year 1 BC is followed by AD 1
     55 	year := p.mustAtoi(str, 0, monSep)
     56 	daySep := monSep + 3
     57 	month := p.mustAtoi(str, monSep+1, daySep)
     58 	p.expect(str, '-', daySep)
     59 	timeSep := daySep + 3
     60 	day := p.mustAtoi(str, daySep+1, timeSep)
     61 
     62 	minLen := monSep + len("01-01") + 1
     63 
     64 	isBC := strings.HasSuffix(str, " BC")
     65 	if isBC {
     66 		minLen += 3
     67 	}
     68 
     69 	var hour, minute, second int
     70 	if len(str) > minLen {
     71 		p.expect(str, ' ', timeSep)
     72 		minSep := timeSep + 3
     73 		p.expect(str, ':', minSep)
     74 		hour = p.mustAtoi(str, timeSep+1, minSep)
     75 		secSep := minSep + 3
     76 		p.expect(str, ':', secSep)
     77 		minute = p.mustAtoi(str, minSep+1, secSep)
     78 		secEnd := secSep + 3
     79 		second = p.mustAtoi(str, secSep+1, secEnd)
     80 	}
     81 	remainderIdx := monSep + len("01-01 00:00:00") + 1
     82 	// Three optional (but ordered) sections follow: the
     83 	// fractional seconds, the time zone offset, and the BC
     84 	// designation. We set them up here and adjust the other
     85 	// offsets if the preceding sections exist.
     86 
     87 	nanoSec := 0
     88 	tzOff := 0
     89 
     90 	if remainderIdx < len(str) && str[remainderIdx] == '.' {
     91 		fracStart := remainderIdx + 1
     92 		fracOff := strings.IndexAny(str[fracStart:], "-+Z ")
     93 		if fracOff < 0 {
     94 			fracOff = len(str) - fracStart
     95 		}
     96 		fracSec := p.mustAtoi(str, fracStart, fracStart+fracOff)
     97 		nanoSec = fracSec * (1000000000 / int(math.Pow(10, float64(fracOff))))
     98 
     99 		remainderIdx += fracOff + 1
    100 	}
    101 	if tzStart := remainderIdx; tzStart < len(str) && (str[tzStart] == '-' || str[tzStart] == '+') {
    102 		// time zone separator is always '-' or '+' or 'Z' (UTC is +00)
    103 		var tzSign int
    104 		switch c := str[tzStart]; c {
    105 		case '-':
    106 			tzSign = -1
    107 		case '+':
    108 			tzSign = +1
    109 		default:
    110 			return time.Time{}, fmt.Errorf("expected '-' or '+' at position %v; got %v", tzStart, c)
    111 		}
    112 		tzHours := p.mustAtoi(str, tzStart+1, tzStart+3)
    113 		remainderIdx += 3
    114 		var tzMin, tzSec int
    115 		if remainderIdx < len(str) && str[remainderIdx] == ':' {
    116 			tzMin = p.mustAtoi(str, remainderIdx+1, remainderIdx+3)
    117 			remainderIdx += 3
    118 		}
    119 		if remainderIdx < len(str) && str[remainderIdx] == ':' {
    120 			tzSec = p.mustAtoi(str, remainderIdx+1, remainderIdx+3)
    121 			remainderIdx += 3
    122 		}
    123 		tzOff = tzSign * ((tzHours * 60 * 60) + (tzMin * 60) + tzSec)
    124 	} else if tzStart < len(str) && str[tzStart] == 'Z' {
    125 		// time zone Z separator indicates UTC is +00
    126 		remainderIdx += 1
    127 	}
    128 
    129 	var isoYear int
    130 
    131 	if isBC {
    132 		isoYear = 1 - year
    133 		remainderIdx += 3
    134 	} else {
    135 		isoYear = year
    136 	}
    137 	if remainderIdx < len(str) {
    138 		return time.Time{}, fmt.Errorf("expected end of input, got %v", str[remainderIdx:])
    139 	}
    140 	t := time.Date(isoYear, time.Month(month), day,
    141 		hour, minute, second, nanoSec,
    142 		globalLocationCache.getLocation(tzOff))
    143 
    144 	if currentLocation != nil {
    145 		// Set the location of the returned Time based on the session's
    146 		// TimeZone value, but only if the local time zone database agrees with
    147 		// the remote database on the offset.
    148 		lt := t.In(currentLocation)
    149 		_, newOff := lt.Zone()
    150 		if newOff == tzOff {
    151 			t = lt
    152 		}
    153 	}
    154 
    155 	return t, p.err
    156 }
    157 
    158 // Format into Postgres' text format for timestamps.
    159 func Format(t time.Time) []byte {
    160 	// Need to send dates before 0001 A.D. with " BC" suffix, instead of the
    161 	// minus sign preferred by Go.
    162 	// Beware, "0000" in ISO is "1 BC", "-0001" is "2 BC" and so on
    163 	bc := false
    164 	if t.Year() <= 0 {
    165 		// flip year sign, and add 1, e.g: "0" will be "1", and "-10" will be "11"
    166 		t = t.AddDate((-t.Year())*2+1, 0, 0)
    167 		bc = true
    168 	}
    169 	b := []byte(t.Format("2006-01-02 15:04:05.999999999Z07:00"))
    170 
    171 	_, offset := t.Zone()
    172 	offset %= 60
    173 	if offset != 0 {
    174 		// RFC3339Nano already printed the minus sign
    175 		if offset < 0 {
    176 			offset = -offset
    177 		}
    178 
    179 		b = append(b, ':')
    180 		if offset < 10 {
    181 			b = append(b, '0')
    182 		}
    183 		b = strconv.AppendInt(b, int64(offset), 10)
    184 	}
    185 
    186 	if bc {
    187 		b = append(b, " BC"...)
    188 	}
    189 	return b
    190 }