1 /**
2  * Boost Software License - Version 1.0 - August 17th, 2003
3  *
4  * Permission is hereby granted, free of charge, to any person or organization
5  * obtaining a copy of the software and accompanying documentation covered by
6  * this license (the "Software") to use, reproduce, display, distribute,
7  * execute, and transmit the Software, and to prepare derivative works of the
8  * Software, and to permit third-parties to whom the Software is furnished to
9  * do so, all subject to the following:
10  *
11  * The copyright notices in the Software and this entire statement, including
12  * the above license grant, this restriction and the following disclaimer,
13  * must be included in all copies of the Software, in whole or in part, and
14  * all derivative works of the Software, unless such copies or derivative
15  * works are solely in the form of machine-executable object code generated by
16  * a source language processor.
17  *
18  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20  * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
21  * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
22  * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
23  * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24  * DEALINGS IN THE SOFTWARE.
25  */
26 
27 module dateparser.ymd;
28 
29 debug(dateparser) import std.stdio;
30 import std.traits;
31 import std.range;
32 
33 package:
34 
35 struct YMD
36 {
37 private:
38     bool century_specified = false;
39     int[3] data;
40     byte dataPosition;
41 
42 public:
43     /**
44      * Params
45      */
46     static bool couldBeYear(Range, N)(Range token, N year) if (isInputRange!Range
47             && isSomeChar!(ElementEncodingType!Range) && is(NumericTypeOf!N : int))
48     {
49         import std.algorithm.comparison : equal;
50         import std.algorithm.mutation : stripLeft;
51         import std.ascii : isDigit;
52         import std.conv : toChars;
53 
54         if (token.front.isDigit)
55         {
56             return year.toChars.equal(token.stripLeft('0'));
57         }
58         else
59         {
60             return false;
61         }
62     }
63 
64     /**
65      * Attempt to deduce if a pre 100 year was lost due to padded zeros being
66      * taken off
67      *
68      * Params:
69      *     tokens = a range of tokens
70      * Returns:
71      *     the index of the year token. If no probable result was found, then -1
72      *     is returned
73      */
74     int probableYearIndex(Range)(Range tokens) const if (isInputRange!Range
75             && isNarrowString!(ElementType!(Range)))
76     {
77         import std.algorithm.iteration : filter;
78         import std.range : walkLength;
79 
80         foreach (int index, ref item; data[])
81         {
82             auto potentialYearTokens = tokens.filter!(a => YMD.couldBeYear(a, item));
83             immutable frontLength = potentialYearTokens.front.length;
84             immutable length = potentialYearTokens.walkLength(2);
85 
86             if (length == 1 && frontLength > 2)
87                 return index;
88         }
89 
90         return -1;
91     }
92 
93     /// Put a value in that represents a year, month, or day
94     void put(N)(N val) if (isNumeric!N)
95     in
96     {
97         assert(dataPosition <= 3);
98     }
99     body
100     {
101         static if (is(N : int))
102         {
103             if (val > 100)
104                 this.century_specified = true;
105 
106             data[dataPosition] = val;
107             ++dataPosition;
108         }
109         else
110             put(cast(int) val);
111     }
112 
113     /// ditto
114     void put(S)(const S val) if (isSomeString!S)
115     in
116     {
117         assert(dataPosition <= 3);
118     }
119     body
120     {
121         import std.conv : to;
122 
123         data[dataPosition] = to!int(val);
124         ++dataPosition;
125 
126         if (val.length > 2)
127             this.century_specified = true;
128     }
129 
130     /// length getter
131     size_t length() @property const @safe pure nothrow @nogc
132     {
133         return dataPosition;
134     }
135 
136     /// century_specified getter
137     bool centurySpecified() @property const @safe pure nothrow @nogc
138     {
139         return century_specified;
140     }
141 
142     /**
143      * Turns the array of ints into a `Tuple` of three, representing the year,
144      * month, and day.
145      *
146      * Params:
147      *     mstridx = The index of the month in the data
148      *     yearfirst = if the year is first in the string
149      *     dayfirst = if the day is first in the string
150      * Returns:
151      *     tuple of three ints
152      */
153     auto resolveYMD(R, N)(R tokens, N mstridx, bool yearfirst, bool dayfirst) if (is(NumericTypeOf!N : size_t))
154     {
155         import std.algorithm.mutation : remove;
156         import std.typecons : tuple;
157 
158         int year = -1;
159         int month;
160         int day;
161 
162         if (dataPosition == 1 || (mstridx != -1 && dataPosition == 2)) //One member, or two members with a month string
163         {
164             if (mstridx != -1)
165             {
166                 month = data[mstridx];
167                 switch (mstridx)
168                 {
169                     case 0:
170                         data[0] = data[1];
171                         data[1] = data[2];
172                         data[2] = 0;
173                         break;
174                     case 1:
175                         data[1] = data[2];
176                         data[2] = 0;
177                         break;
178                     case 2:
179                         data[2] = 0;
180                         break;
181                     default: break;
182                 }                
183             }
184 
185             if (dataPosition > 1 || mstridx == -1)
186             {
187                 if (data[0] > 31)
188                     year = data[0];
189                 else
190                     day = data[0];
191             }
192         }
193         else if (dataPosition == 2) //Two members with numbers
194         {
195             if (data[0] > 31)
196             {
197                 //99-01
198                 year = data[0];
199                 month = data[1];
200             }
201             else if (data[1] > 31)
202             {
203                 //01-99
204                 month = data[0];
205                 year = data[1];
206             }
207             else if (dayfirst && data[1] <= 12)
208             {
209                 //13-01
210                 day = data[0];
211                 month = data[1];
212             }
213             else
214             {
215                 //01-13
216                 month = data[0];
217                 day = data[1];
218             }
219 
220         }
221         else if (dataPosition == 3) //Three members
222         {
223             if (mstridx == 0)
224             {
225                 month = data[0];
226                 day = data[1];
227                 year = data[2];
228             }
229             else if (mstridx == 1)
230             {
231                 if (data[0] > 31 || (yearfirst && data[2] <= 31))
232                 {
233                     //99-Jan-01
234                     year = data[0];
235                     month = data[1];
236                     day = data[2];
237                 }
238                 else
239                 {
240                     //01-Jan-01
241                     //Give precedence to day-first, since
242                     //two-digit years is usually hand-written.
243                     day = data[0];
244                     month = data[1];
245                     year = data[2];
246                 }
247             }
248             else if (mstridx == 2)
249             {
250                 if (data[1] > 31)
251                 {
252                     //01-99-Jan
253                     day = data[0];
254                     year = data[1];
255                     month = data[2];
256                 }
257                 else
258                 {
259                     //99-01-Jan
260                     year = data[0];
261                     day = data[1];
262                     month = data[2];
263                 }
264             }
265             else
266             {
267                 if (data[0] > 31 || probableYearIndex(tokens) == 0
268                         || (yearfirst && data[1] <= 12 && data[2] <= 31))
269                 {
270                     //99-01-01
271                     year = data[0];
272                     month = data[1];
273                     day = data[2];
274                 }
275                 else if (data[0] > 12 || (dayfirst && data[1] <= 12))
276                 {
277                     //13-01-01
278                     day = data[0];
279                     month = data[1];
280                     year = data[2];
281                 }
282                 else
283                 {
284                     //01-13-01
285                     month = data[0];
286                     day = data[1];
287                     year = data[2];
288                 }
289             }
290         }
291 
292         return tuple(year, month, day);
293     }
294 }