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 import std.compiler;
33 
34 package:
35 
36 struct YMD
37 {
38 private:
39     bool century_specified = false;
40     int[3] data;
41     byte dataPosition;
42 
43 public:
44     /**
45      * Params
46      */
47     static bool couldBeYear(Range, N)(Range token, N year) if (isInputRange!Range
48             && isSomeChar!(ElementEncodingType!Range) && is(NumericTypeOf!N : int))
49     {
50         import std.uni : isNumber;
51         import std.exception : assumeWontThrow;
52         import std.conv : to;
53         import std.algorithm.mutation : stripLeft;
54 
55         if (token.front.isNumber)
56         {
57             static if (version_major == 2 && version_minor >= 69)
58             {
59                 import std.algorithm.comparison : equal;
60                 import std.conv : toChars;
61 
62                 return year.toChars.equal(token.stripLeft('0'));
63             }
64             else
65                 return assumeWontThrow(to!int(token)) == year;
66         }
67         else
68             return false;
69     }
70 
71     /**
72      * Attempt to deduce if a pre 100 year was lost due to padded zeros being
73      * taken off
74      *
75      * Params:
76      *     tokens = a range of tokens
77      * Returns:
78      *     the index of the year token. If no probable result was found, then -1
79      *     is returned
80      */
81     int probableYearIndex(Range)(Range tokens) const if (isInputRange!Range
82             && isNarrowString!(ElementType!(Range)))
83     {
84         import std.algorithm.iteration : filter;
85         import std.range : walkLength;
86 
87         foreach (int index, ref item; data[])
88         {
89             auto potentialYearTokens = tokens.filter!(a => YMD.couldBeYear(a, item));
90             immutable frontLength = potentialYearTokens.front.length;
91             immutable length = potentialYearTokens.walkLength(2);
92 
93             if (length == 1 && frontLength > 2)
94                 return index;
95         }
96 
97         return -1;
98     }
99 
100     /// Put a value in that represents a year, month, or day
101     void put(N)(N val) if (isNumeric!N)
102     in
103     {
104         assert(dataPosition <= 3);
105     }
106     body
107     {
108         static if (is(N : int))
109         {
110             if (val > 100)
111                 this.century_specified = true;
112 
113             data[dataPosition] = val;
114             ++dataPosition;
115         }
116         else
117             put(cast(int) val);
118     }
119 
120     /// ditto
121     void put(S)(const S val) if (isSomeString!S)
122     in
123     {
124         assert(dataPosition <= 3);
125     }
126     body
127     {
128         import std.conv : to;
129 
130         data[dataPosition] = to!int(val);
131         ++dataPosition;
132 
133         if (val.length > 2)
134             this.century_specified = true;
135     }
136 
137     /// length getter
138     size_t length() @property const @safe pure nothrow @nogc
139     {
140         return dataPosition;
141     }
142 
143     /// century_specified getter
144     bool centurySpecified() @property const @safe pure nothrow @nogc
145     {
146         return century_specified;
147     }
148 
149     /**
150      * Turns the array of ints into a `Tuple` of three, representing the year,
151      * month, and day.
152      *
153      * Params:
154      *     mstridx = The index of the month in the data
155      *     yearfirst = if the year is first in the string
156      *     dayfirst = if the day is first in the string
157      * Returns:
158      *     tuple of three ints
159      */
160     auto resolveYMD(R, N)(R tokens, N mstridx, bool yearfirst, bool dayfirst) if (is(NumericTypeOf!N : size_t))
161     {
162         import std.algorithm.mutation : remove;
163         import std.typecons : tuple;
164 
165         int year = -1;
166         int month;
167         int day;
168 
169         if (dataPosition == 1 || (mstridx != -1 && dataPosition == 2)) //One member, or two members with a month string
170         {
171             if (mstridx != -1)
172             {
173                 month = data[mstridx];
174                 switch (mstridx)
175                 {
176                     case 0:
177                         data[0] = data[1];
178                         data[1] = data[2];
179                         data[2] = 0;
180                         break;
181                     case 1:
182                         data[1] = data[2];
183                         data[2] = 0;
184                         break;
185                     case 2:
186                         data[2] = 0;
187                         break;
188                     default: break;
189                 }                
190             }
191 
192             if (dataPosition > 1 || mstridx == -1)
193             {
194                 if (data[0] > 31)
195                     year = data[0];
196                 else
197                     day = data[0];
198             }
199         }
200         else if (dataPosition == 2) //Two members with numbers
201         {
202             if (data[0] > 31)
203             {
204                 //99-01
205                 year = data[0];
206                 month = data[1];
207             }
208             else if (data[1] > 31)
209             {
210                 //01-99
211                 month = data[0];
212                 year = data[1];
213             }
214             else if (dayfirst && data[1] <= 12)
215             {
216                 //13-01
217                 day = data[0];
218                 month = data[1];
219             }
220             else
221             {
222                 //01-13
223                 month = data[0];
224                 day = data[1];
225             }
226 
227         }
228         else if (dataPosition == 3) //Three members
229         {
230             if (mstridx == 0)
231             {
232                 month = data[0];
233                 day = data[1];
234                 year = data[2];
235             }
236             else if (mstridx == 1)
237             {
238                 if (data[0] > 31 || (yearfirst && data[2] <= 31))
239                 {
240                     //99-Jan-01
241                     year = data[0];
242                     month = data[1];
243                     day = data[2];
244                 }
245                 else
246                 {
247                     //01-Jan-01
248                     //Give precedence to day-first, since
249                     //two-digit years is usually hand-written.
250                     day = data[0];
251                     month = data[1];
252                     year = data[2];
253                 }
254             }
255             else if (mstridx == 2)
256             {
257                 if (data[1] > 31)
258                 {
259                     //01-99-Jan
260                     day = data[0];
261                     year = data[1];
262                     month = data[2];
263                 }
264                 else
265                 {
266                     //99-01-Jan
267                     year = data[0];
268                     day = data[1];
269                     month = data[2];
270                 }
271             }
272             else
273             {
274                 if (data[0] > 31 || probableYearIndex(tokens) == 0
275                         || (yearfirst && data[1] <= 12 && data[2] <= 31))
276                 {
277                     //99-01-01
278                     year = data[0];
279                     month = data[1];
280                     day = data[2];
281                 }
282                 else if (data[0] > 12 || (dayfirst && data[1] <= 12))
283                 {
284                     //13-01-01
285                     day = data[0];
286                     month = data[1];
287                     year = data[2];
288                 }
289                 else
290                 {
291                     //01-13-01
292                     month = data[0];
293                     day = data[1];
294                     year = data[2];
295                 }
296             }
297         }
298 
299         return tuple(year, month, day);
300     }
301 }