summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/rickb777/date/period/parse.go
blob: 30b3a6931bf42fe967a1bc124a82391464af7aef (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
// Copyright 2015 Rick Beton. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package period

import (
	"fmt"
	"strconv"
	"strings"
)

// MustParse is as per Parse except that it panics if the string cannot be parsed.
// This is intended for setup code; don't use it for user inputs.
func MustParse(value string) Period {
	d, err := Parse(value)
	if err != nil {
		panic(err)
	}
	return d
}

// Parse parses strings that specify periods using ISO-8601 rules.
//
// In addition, a plus or minus sign can precede the period, e.g. "-P10D"
//
// The value is normalised, e.g. multiple of 12 months become years so "P24M"
// is the same as "P2Y". However, this is done without loss of precision, so
// for example whole numbers of days do not contribute to the months tally
// because the number of days per month is variable.
//
// The zero value can be represented in several ways: all of the following
// are equivalent: "P0Y", "P0M", "P0W", "P0D", "PT0H", PT0M", PT0S", and "P0".
// The canonical zero is "P0D".
func Parse(period string) (Period, error) {
	if period == "" {
		return Period{}, fmt.Errorf("cannot parse a blank string as a period")
	}

	if period == "P0" {
		return Period{}, nil
	}

	result := period64{}
	pcopy := period
	if pcopy[0] == '-' {
		result.neg = true
		pcopy = pcopy[1:]
	} else if pcopy[0] == '+' {
		pcopy = pcopy[1:]
	}

	if pcopy[0] != 'P' {
		return Period{}, fmt.Errorf("expected 'P' period mark at the start: %s", period)
	}
	pcopy = pcopy[1:]

	st := parseState{period, pcopy, false, nil}
	t := strings.IndexByte(pcopy, 'T')
	if t >= 0 {
		st.pcopy = pcopy[t+1:]

		result.hours, st = parseField(st, 'H')
		if st.err != nil {
			return Period{}, fmt.Errorf("expected a number before the 'H' marker: %s", period)
		}

		result.minutes, st = parseField(st, 'M')
		if st.err != nil {
			return Period{}, fmt.Errorf("expected a number before the 'M' marker: %s", period)
		}

		result.seconds, st = parseField(st, 'S')
		if st.err != nil {
			return Period{}, fmt.Errorf("expected a number before the 'S' marker: %s", period)
		}

		st.pcopy = pcopy[:t]
	}

	result.years, st = parseField(st, 'Y')
	if st.err != nil {
		return Period{}, fmt.Errorf("expected a number before the 'Y' marker: %s", period)
	}

	result.months, st = parseField(st, 'M')
	if st.err != nil {
		return Period{}, fmt.Errorf("expected a number before the 'M' marker: %s", period)
	}

	weeks, st := parseField(st, 'W')
	if st.err != nil {
		return Period{}, fmt.Errorf("expected a number before the 'W' marker: %s", period)
	}

	days, st := parseField(st, 'D')
	if st.err != nil {
		return Period{}, fmt.Errorf("expected a number before the 'D' marker: %s", period)
	}

	result.days = weeks*7 + days
	//fmt.Printf("%#v\n", st)

	if !st.ok {
		return Period{}, fmt.Errorf("expected 'Y', 'M', 'W', 'D', 'H', 'M', or 'S' marker: %s", period)
	}

	return result.normalise64(true).toPeriod(), nil
}

type parseState struct {
	period, pcopy string
	ok            bool
	err           error
}

func parseField(st parseState, mark byte) (int64, parseState) {
	//fmt.Printf("%c %#v\n", mark, st)
	r := int64(0)
	m := strings.IndexByte(st.pcopy, mark)
	if m > 0 {
		r, st.err = parseDecimalFixedPoint(st.pcopy[:m], st.period)
		if st.err != nil {
			return 0, st
		}
		st.pcopy = st.pcopy[m+1:]
		st.ok = true
	}
	return r, st
}

// Fixed-point three decimal places
func parseDecimalFixedPoint(s, original string) (int64, error) {
	//was := s
	dec := strings.IndexByte(s, '.')
	if dec < 0 {
		dec = strings.IndexByte(s, ',')
	}

	if dec >= 0 {
		dp := len(s) - dec
		if dp > 1 {
			s = s[:dec] + s[dec+1:dec+2]
		} else {
			s = s[:dec] + s[dec+1:] + "0"
		}
	} else {
		s = s + "0"
	}

	return strconv.ParseInt(s, 10, 64)
}