1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | /// @docImport 'calendar_date_picker.dart'; |
6 | /// @docImport 'date_picker.dart'; |
7 | /// @docImport 'text_field.dart'; |
8 | library; |
9 | |
10 | import 'package:flutter/foundation.dart'; |
11 | |
12 | import 'material_localizations.dart'; |
13 | |
14 | /// Utility functions for working with dates. |
15 | abstract final class DateUtils { |
16 | /// Returns a [DateTime] with the date of the original, but time set to |
17 | /// midnight. |
18 | static DateTime dateOnly(DateTime date) { |
19 | return DateTime(date.year, date.month, date.day); |
20 | } |
21 | |
22 | /// Returns a [DateTimeRange] with the dates of the original, but with times |
23 | /// set to midnight. |
24 | /// |
25 | /// See also: |
26 | /// * [dateOnly], which does the same thing for a single date. |
27 | static DateTimeRange datesOnly(DateTimeRange range) { |
28 | return DateTimeRange(start: dateOnly(range.start), end: dateOnly(range.end)); |
29 | } |
30 | |
31 | /// Returns true if the two [DateTime] objects have the same day, month, and |
32 | /// year, or are both null. |
33 | static bool isSameDay(DateTime? dateA, DateTime? dateB) { |
34 | return |
35 | dateA?.year == dateB?.year && |
36 | dateA?.month == dateB?.month && |
37 | dateA?.day == dateB?.day; |
38 | } |
39 | |
40 | /// Returns true if the two [DateTime] objects have the same month and |
41 | /// year, or are both null. |
42 | static bool isSameMonth(DateTime? dateA, DateTime? dateB) { |
43 | return |
44 | dateA?.year == dateB?.year && |
45 | dateA?.month == dateB?.month; |
46 | } |
47 | |
48 | /// Determines the number of months between two [DateTime] objects. |
49 | /// |
50 | /// For example: |
51 | /// |
52 | /// ```dart |
53 | /// DateTime date1 = DateTime(2019, 6, 15); |
54 | /// DateTime date2 = DateTime(2020, 1, 15); |
55 | /// int delta = DateUtils.monthDelta(date1, date2); |
56 | /// ``` |
57 | /// |
58 | /// The value for `delta` would be `7`. |
59 | static int monthDelta(DateTime startDate, DateTime endDate) { |
60 | return (endDate.year - startDate.year) * 12 + endDate.month - startDate.month; |
61 | } |
62 | |
63 | /// Returns a [DateTime] that is [monthDate] with the added number |
64 | /// of months and the day set to 1 and time set to midnight. |
65 | /// |
66 | /// For example: |
67 | /// |
68 | /// ```dart |
69 | /// DateTime date = DateTime(2019, 1, 15); |
70 | /// DateTime futureDate = DateUtils.addMonthsToMonthDate(date, 3); |
71 | /// ``` |
72 | /// |
73 | /// `date` would be January 15, 2019. |
74 | /// `futureDate` would be April 1, 2019 since it adds 3 months. |
75 | static DateTime addMonthsToMonthDate(DateTime monthDate, int monthsToAdd) { |
76 | return DateTime(monthDate.year, monthDate.month + monthsToAdd); |
77 | } |
78 | |
79 | /// Returns a [DateTime] with the added number of days and time set to |
80 | /// midnight. |
81 | static DateTime addDaysToDate(DateTime date, int days) { |
82 | return DateTime(date.year, date.month, date.day + days); |
83 | } |
84 | |
85 | /// Computes the offset from the first day of the week that the first day of |
86 | /// the [month] falls on. |
87 | /// |
88 | /// For example, September 1, 2017 falls on a Friday, which in the calendar |
89 | /// localized for United States English appears as: |
90 | /// |
91 | /// S M T W T F S |
92 | /// _ _ _ _ _ 1 2 |
93 | /// |
94 | /// The offset for the first day of the months is the number of leading blanks |
95 | /// in the calendar, i.e. 5. |
96 | /// |
97 | /// The same date localized for the Russian calendar has a different offset, |
98 | /// because the first day of week is Monday rather than Sunday: |
99 | /// |
100 | /// M T W T F S S |
101 | /// _ _ _ _ 1 2 3 |
102 | /// |
103 | /// So the offset is 4, rather than 5. |
104 | /// |
105 | /// This code consolidates the following: |
106 | /// |
107 | /// - [DateTime.weekday] provides a 1-based index into days of week, with 1 |
108 | /// falling on Monday. |
109 | /// - [MaterialLocalizations.firstDayOfWeekIndex] provides a 0-based index |
110 | /// into the [MaterialLocalizations.narrowWeekdays] list. |
111 | /// - [MaterialLocalizations.narrowWeekdays] list provides localized names of |
112 | /// days of week, always starting with Sunday and ending with Saturday. |
113 | static int firstDayOffset(int year, int month, MaterialLocalizations localizations) { |
114 | // 0-based day of week for the month and year, with 0 representing Monday. |
115 | final int weekdayFromMonday = DateTime(year, month).weekday - 1; |
116 | |
117 | // 0-based start of week depending on the locale, with 0 representing Sunday. |
118 | int firstDayOfWeekIndex = localizations.firstDayOfWeekIndex; |
119 | |
120 | // firstDayOfWeekIndex recomputed to be Monday-based, in order to compare with |
121 | // weekdayFromMonday. |
122 | firstDayOfWeekIndex = (firstDayOfWeekIndex - 1) % 7; |
123 | |
124 | // Number of days between the first day of week appearing on the calendar, |
125 | // and the day corresponding to the first of the month. |
126 | return (weekdayFromMonday - firstDayOfWeekIndex) % 7; |
127 | } |
128 | |
129 | /// Returns the number of days in a month, according to the proleptic |
130 | /// Gregorian calendar. |
131 | /// |
132 | /// This applies the leap year logic introduced by the Gregorian reforms of |
133 | /// 1582. It will not give valid results for dates prior to that time. |
134 | static int getDaysInMonth(int year, int month) { |
135 | if (month == DateTime.february) { |
136 | final bool isLeapYear = (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0); |
137 | return isLeapYear ? 29 : 28; |
138 | } |
139 | const List<int> daysInMonth = <int>[31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; |
140 | return daysInMonth[month - 1]; |
141 | } |
142 | } |
143 | |
144 | /// Mode of date entry method for the date picker dialog. |
145 | /// |
146 | /// In [calendar] mode, a calendar grid is displayed and the user taps the |
147 | /// day they wish to select. In [input] mode, a [TextField] is displayed and |
148 | /// the user types in the date they wish to select. |
149 | /// |
150 | /// [calendarOnly] and [inputOnly] are variants of the above that don't |
151 | /// allow the user to change to the mode. |
152 | /// |
153 | /// See also: |
154 | /// |
155 | /// * [showDatePicker] and [showDateRangePicker], which use this to control |
156 | /// the initial entry mode of their dialogs. |
157 | enum DatePickerEntryMode { |
158 | /// User picks a date from calendar grid. Can switch to [input] by activating |
159 | /// a mode button in the dialog. |
160 | calendar, |
161 | |
162 | /// User can input the date by typing it into a text field. |
163 | /// |
164 | /// Can switch to [calendar] by activating a mode button in the dialog. |
165 | input, |
166 | |
167 | /// User can only pick a date from calendar grid. |
168 | /// |
169 | /// There is no user interface to switch to another mode. |
170 | calendarOnly, |
171 | |
172 | /// User can only input the date by typing it into a text field. |
173 | /// |
174 | /// There is no user interface to switch to another mode. |
175 | inputOnly, |
176 | } |
177 | |
178 | /// Initial display of a calendar date picker. |
179 | /// |
180 | /// Either a grid of available years or a monthly calendar. |
181 | /// |
182 | /// See also: |
183 | /// |
184 | /// * [showDatePicker], which shows a dialog that contains a Material Design |
185 | /// date picker. |
186 | /// * [CalendarDatePicker], widget which implements the Material Design date picker. |
187 | enum DatePickerMode { |
188 | /// Choosing a month and day. |
189 | day, |
190 | |
191 | /// Choosing a year. |
192 | year, |
193 | } |
194 | |
195 | /// Signature for predicating dates for enabled date selections. |
196 | /// |
197 | /// See [showDatePicker], which has a [SelectableDayPredicate] parameter used |
198 | /// to specify allowable days in the date picker. |
199 | typedef SelectableDayPredicate = bool Function(DateTime day); |
200 | |
201 | /// Encapsulates a start and end [DateTime] that represent the range of dates. |
202 | /// |
203 | /// The range includes the [start] and [end] dates. The [start] and [end] dates |
204 | /// may be equal to indicate a date range of a single day. The [start] date must |
205 | /// not be after the [end] date. |
206 | /// |
207 | /// See also: |
208 | /// * [showDateRangePicker], which displays a dialog that allows the user to |
209 | /// select a date range. |
210 | @immutable |
211 | class DateTimeRange { |
212 | /// Creates a date range for the given start and end [DateTime]. |
213 | DateTimeRange({ |
214 | required this.start, |
215 | required this.end, |
216 | }) : assert(!start.isAfter(end)); |
217 | |
218 | /// The start of the range of dates. |
219 | final DateTime start; |
220 | |
221 | /// The end of the range of dates. |
222 | final DateTime end; |
223 | |
224 | /// Returns a [Duration] of the time between [start] and [end]. |
225 | /// |
226 | /// See [DateTime.difference] for more details. |
227 | Duration get duration => end.difference(start); |
228 | |
229 | @override |
230 | bool operator ==(Object other) { |
231 | if (other.runtimeType != runtimeType) { |
232 | return false; |
233 | } |
234 | return other is DateTimeRange |
235 | && other.start == start |
236 | && other.end == end; |
237 | } |
238 | |
239 | @override |
240 | int get hashCode => Object.hash(start, end); |
241 | |
242 | @override |
243 | String toString() => ' $start - $end' ; |
244 | } |
245 | |