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 dateparser2.ymd;
28 
29 debug(dateparser2) 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 (index, ref item; data[])
81         {
82             auto potentialYearTokens = tokens.filter!(a => YMD.couldBeYear(a, item));
83 			if(potentialYearTokens.empty) {
84 				return -1;
85 			}
86             immutable frontLength = potentialYearTokens.front.length;
87             immutable length = potentialYearTokens.walkLength(2);
88 
89             if (length == 1 && frontLength > 2)
90                 return cast(int) index;
91         }
92 
93         return -1;
94     }
95 
96     /// Put a value in that represents a year, month, or day
97     void put(N)(N val) if (isNumeric!N)
98     in
99     {
100         assert(dataPosition <= 3);
101     }
102     do
103     {
104         static if (is(N : int))
105         {
106             if (val > 100)
107                 this.century_specified = true;
108 
109             data[dataPosition] = val;
110             ++dataPosition;
111         }
112         else
113             put(cast(int) val);
114     }
115 
116     /// ditto
117     void put(S)(const S val) if (isSomeString!S)
118     in
119     {
120         assert(dataPosition <= 3);
121     }
122     do
123     {
124         import std.conv : to;
125 
126         data[dataPosition] = to!int(val);
127         ++dataPosition;
128 
129         if (val.length > 2)
130             this.century_specified = true;
131     }
132 
133     /// length getter
134     size_t length() @property const @safe pure nothrow @nogc
135     {
136         return dataPosition;
137     }
138 
139     /// century_specified getter
140     bool centurySpecified() @property const @safe pure nothrow @nogc
141     {
142         return century_specified;
143     }
144 
145     /**
146      * Turns the array of ints into a `Tuple` of three, representing the year,
147      * month, and day.
148      *
149      * Params:
150      *     mstridx = The index of the month in the data
151      *     yearfirst = if the year is first in the string
152      *     dayfirst = if the day is first in the string
153      * Returns:
154      *     tuple of three ints
155      */
156     auto resolveYMD(R, N)(R tokens, N mstridx, bool yearfirst, bool dayfirst) if (is(NumericTypeOf!N : size_t))
157     {
158         import std.algorithm.mutation : remove;
159         import std.typecons : tuple;
160 
161         int year = -1;
162         int month;
163         int day;
164 
165         if (dataPosition == 1 || (mstridx != -1 && dataPosition == 2)) //One member, or two members with a month string
166         {
167             if (mstridx != -1)
168             {
169                 month = data[mstridx];
170                 switch (mstridx)
171                 {
172                     case 0:
173                         data[0] = data[1];
174                         data[1] = data[2];
175                         data[2] = 0;
176                         break;
177                     case 1:
178                         data[1] = data[2];
179                         data[2] = 0;
180                         break;
181                     case 2:
182                         data[2] = 0;
183                         break;
184                     default: break;
185                 }
186             }
187 
188             if (dataPosition > 1 || mstridx == -1)
189             {
190                 if (data[0] > 31)
191                     year = data[0];
192                 else
193                     day = data[0];
194             }
195         }
196         else if (dataPosition == 2) //Two members with numbers
197         {
198             if (data[0] > 31)
199             {
200                 //99-01
201                 year = data[0];
202                 month = data[1];
203             }
204             else if (data[1] > 31)
205             {
206                 //01-99
207                 month = data[0];
208                 year = data[1];
209             }
210             else if (dayfirst && data[1] <= 12)
211             {
212                 //13-01
213                 day = data[0];
214                 month = data[1];
215             }
216             else
217             {
218                 //01-13
219                 month = data[0];
220                 day = data[1];
221             }
222 
223         }
224         else if (dataPosition == 3) //Three members
225         {
226             if (mstridx == 0)
227             {
228                 month = data[0];
229                 day = data[1];
230                 year = data[2];
231             }
232             else if (mstridx == 1)
233             {
234                 if (data[0] > 31 || (yearfirst && data[2] <= 31))
235                 {
236                     //99-Jan-01
237                     year = data[0];
238                     month = data[1];
239                     day = data[2];
240                 }
241                 else
242                 {
243                     //01-Jan-01
244                     //Give precedence to day-first, since
245                     //two-digit years is usually hand-written.
246                     day = data[0];
247                     month = data[1];
248                     year = data[2];
249                 }
250             }
251             else if (mstridx == 2)
252             {
253                 if (data[1] > 31)
254                 {
255                     //01-99-Jan
256                     day = data[0];
257                     year = data[1];
258                     month = data[2];
259                 }
260                 else
261                 {
262                     //99-01-Jan
263                     year = data[0];
264                     day = data[1];
265                     month = data[2];
266                 }
267             }
268             else
269             {
270                 if (data[0] > 31 || probableYearIndex(tokens) == 0
271                         || (yearfirst && data[1] <= 12 && data[2] <= 31))
272                 {
273                     //99-01-01
274                     year = data[0];
275                     month = data[1];
276                     day = data[2];
277                 }
278                 else if (data[0] > 12 || (dayfirst && data[1] <= 12))
279                 {
280                     //13-01-01
281                     day = data[0];
282                     month = data[1];
283                     year = data[2];
284                 }
285                 else
286                 {
287                     //01-13-01
288                     month = data[0];
289                     day = data[1];
290                     year = data[2];
291                 }
292             }
293         }
294 
295         return tuple(year, month, day);
296     }
297 }