Skip to content

Commit 41a30da

Browse files
committed
Some more tests and date validation
Signed-off-by: Leo Berman <leograntberman@gmail.com>
1 parent 4df09a5 commit 41a30da

File tree

2 files changed

+150
-24
lines changed

2 files changed

+150
-24
lines changed

app/src/main/java/com/owncloud/android/ui/adapter/GalleryAdapter.kt

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -61,33 +61,49 @@ class GalleryAdapter(
6161
companion object {
6262
private const val TAG = "GalleryAdapter"
6363
private const val FIRST_DAY_OF_MONTH = 1
64-
// Pattern to extract YYYY/MM or YYYY/MM/DD from file path (requires zero-padded month/day)
65-
private val FOLDER_DATE_PATTERN: Pattern = Pattern.compile("/(\\d{4})/(\\d{2})(?:/(\\d{2}))?/")
64+
private const val FIRST_MONTH = 1
65+
private const val YEAR_GROUP = 1
66+
private const val MONTH_GROUP = 2
67+
private const val DAY_GROUP = 3
68+
69+
// Pattern to extract YYYY, YYYY/MM, or YYYY/MM/DD from file path (requires zero-padded month/day)
70+
private val FOLDER_DATE_PATTERN: Pattern = Pattern.compile("/(\\d{4})(?:/(\\d{2}))?(?:/(\\d{2}))?/")
6671

6772
/**
68-
* Extract folder date from path (YYYY/MM or YYYY/MM/DD).
69-
* @return timestamp or null if no folder date found
73+
* Extract folder date from path (YYYY, YYYY/MM, or YYYY/MM/DD).
74+
* Uses LocalDate for calendar-aware validation (leap years, days per month).
75+
* Invalid month/day values fall back to defaults. Future dates are rejected.
76+
* @return timestamp or null if no folder date found or date is in the future
7077
*/
7178
@VisibleForTesting
79+
@Suppress("TooGenericExceptionCaught")
7280
fun extractFolderDate(path: String?): Long? {
73-
val matcher = path?.let { FOLDER_DATE_PATTERN.matcher(it) } ?: return null
74-
if (!matcher.find()) return null
75-
76-
val year = matcher.group(1)?.toIntOrNull()
77-
val month = matcher.group(2)?.toIntOrNull()
78-
val day = matcher.group(3)?.toIntOrNull() ?: FIRST_DAY_OF_MONTH
79-
80-
return if (year != null && month != null) {
81-
Calendar.getInstance().apply {
82-
set(Calendar.YEAR, year)
83-
set(Calendar.MONTH, month - 1)
84-
set(Calendar.DAY_OF_MONTH, day)
85-
set(Calendar.HOUR_OF_DAY, 0)
86-
set(Calendar.MINUTE, 0)
87-
set(Calendar.SECOND, 0)
88-
set(Calendar.MILLISECOND, 0)
89-
}.timeInMillis
90-
} else {
81+
return try {
82+
val matcher = path?.let { FOLDER_DATE_PATTERN.matcher(it) }
83+
if (matcher?.find() != true) return null
84+
val year = matcher.group(YEAR_GROUP)?.toIntOrNull() ?: return null
85+
val rawMonth = matcher.group(MONTH_GROUP)?.toIntOrNull()
86+
val rawDay = matcher.group(DAY_GROUP)?.toIntOrNull()
87+
88+
val month = rawMonth ?: FIRST_MONTH
89+
val day = rawDay ?: FIRST_DAY_OF_MONTH
90+
91+
val localDate = tryCreateDate(year, month, day)
92+
?: tryCreateDate(year, month, FIRST_DAY_OF_MONTH)
93+
?: tryCreateDate(year, FIRST_MONTH, FIRST_DAY_OF_MONTH)
94+
95+
if (localDate?.isAfter(java.time.LocalDate.now()) == true) return null
96+
97+
localDate?.atStartOfDay(java.time.ZoneId.systemDefault())?.toInstant()?.toEpochMilli()
98+
} catch (e: Exception) {
99+
null
100+
}
101+
}
102+
103+
private fun tryCreateDate(year: Int, month: Int, day: Int): java.time.LocalDate? {
104+
return try {
105+
java.time.LocalDate.of(year, month, day)
106+
} catch (e: java.time.DateTimeException) {
91107
null
92108
}
93109
}

app/src/test/java/com/owncloud/android/ui/adapter/GalleryAdapterFolderDateTest.kt

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,15 @@ class GalleryAdapterFolderDateTest {
4949
}
5050

5151
@Test
52-
fun `extractFolderDate rejects single digit month`() {
53-
assertNull(GalleryAdapter.extractFolderDate("/Photos/2025/3/image.jpg"))
52+
fun `extractFolderDate handles single digit month as year only`() {
53+
// Single digit month doesn't match pattern, so only year is captured
54+
val result = GalleryAdapter.extractFolderDate("/Photos/2025/3/image.jpg")
55+
assertNotNull(result)
56+
57+
val cal = Calendar.getInstance().apply { timeInMillis = result!! }
58+
assertEquals(2025, cal.get(Calendar.YEAR))
59+
assertEquals(0, cal.get(Calendar.MONTH)) // defaults to January (0)
60+
assertEquals(1, cal.get(Calendar.DAY_OF_MONTH)) // defaults to 1
5461
}
5562

5663
@Test
@@ -108,4 +115,107 @@ class GalleryAdapterFolderDateTest {
108115
assert(feb01 > jan20) { "Feb 1 should be after Jan 20" }
109116
assert(feb01 > jan15) { "Feb 1 should be after Jan 15" }
110117
}
118+
119+
@Test
120+
fun `extractFolderDate handles year only path`() {
121+
val result = GalleryAdapter.extractFolderDate("/Photos/2025/image.jpg")
122+
assertNotNull(result)
123+
124+
val cal = Calendar.getInstance().apply { timeInMillis = result!! }
125+
assertEquals(2025, cal.get(Calendar.YEAR))
126+
assertEquals(0, cal.get(Calendar.MONTH)) // defaults to January (0)
127+
assertEquals(1, cal.get(Calendar.DAY_OF_MONTH)) // defaults to 1
128+
}
129+
130+
@Test
131+
fun `extractFolderDate handles invalid month 00 as year only`() {
132+
// Month 00 is invalid, so it defaults to January
133+
val result = GalleryAdapter.extractFolderDate("/Photos/2025/00/image.jpg")
134+
assertNotNull(result)
135+
136+
val cal = Calendar.getInstance().apply { timeInMillis = result!! }
137+
assertEquals(2025, cal.get(Calendar.YEAR))
138+
assertEquals(0, cal.get(Calendar.MONTH)) // defaults to January (0)
139+
assertEquals(1, cal.get(Calendar.DAY_OF_MONTH)) // defaults to 1
140+
}
141+
142+
@Test
143+
fun `extractFolderDate handles month 12`() {
144+
val result = GalleryAdapter.extractFolderDate("/Photos/2025/12/image.jpg")
145+
assertNotNull(result)
146+
147+
val cal = Calendar.getInstance().apply { timeInMillis = result!! }
148+
assertEquals(2025, cal.get(Calendar.YEAR))
149+
assertEquals(11, cal.get(Calendar.MONTH)) // December is 11
150+
}
151+
152+
@Test
153+
fun `extractFolderDate handles day 31`() {
154+
val result = GalleryAdapter.extractFolderDate("/Photos/2025/01/31/image.jpg")
155+
assertNotNull(result)
156+
157+
val cal = Calendar.getInstance().apply { timeInMillis = result!! }
158+
assertEquals(31, cal.get(Calendar.DAY_OF_MONTH))
159+
}
160+
161+
@Test
162+
fun `extractFolderDate handles invalid day Feb 30 as Feb 1`() {
163+
// Feb 30 is invalid, so day defaults to 1
164+
val result = GalleryAdapter.extractFolderDate("/Photos/2025/02/30/image.jpg")
165+
assertNotNull(result)
166+
167+
val cal = Calendar.getInstance().apply { timeInMillis = result!! }
168+
assertEquals(2025, cal.get(Calendar.YEAR))
169+
assertEquals(1, cal.get(Calendar.MONTH)) // February is 1
170+
assertEquals(1, cal.get(Calendar.DAY_OF_MONTH)) // defaults to 1
171+
}
172+
173+
@Test
174+
fun `extractFolderDate handles invalid day 00 as day 1`() {
175+
// Day 00 is invalid, so day defaults to 1
176+
val result = GalleryAdapter.extractFolderDate("/Photos/2025/03/00/image.jpg")
177+
assertNotNull(result)
178+
179+
val cal = Calendar.getInstance().apply { timeInMillis = result!! }
180+
assertEquals(2025, cal.get(Calendar.YEAR))
181+
assertEquals(2, cal.get(Calendar.MONTH)) // March is 2
182+
assertEquals(1, cal.get(Calendar.DAY_OF_MONTH)) // defaults to 1
183+
}
184+
185+
@Test
186+
fun `extractFolderDate requires trailing slash after date components`() {
187+
// No trailing slash after month, so only year is captured
188+
val result = GalleryAdapter.extractFolderDate("/Photos/2025/03image.jpg")
189+
assertNotNull(result)
190+
191+
val cal = Calendar.getInstance().apply { timeInMillis = result!! }
192+
assertEquals(2025, cal.get(Calendar.YEAR))
193+
assertEquals(0, cal.get(Calendar.MONTH)) // defaults to January (0)
194+
assertEquals(1, cal.get(Calendar.DAY_OF_MONTH)) // defaults to 1
195+
}
196+
197+
@Test
198+
fun `extractFolderDate returns null when no trailing slash after year`() {
199+
// Pattern requires trailing slash after year at minimum
200+
assertNull(GalleryAdapter.extractFolderDate("/Photos/2025image.jpg"))
201+
}
202+
203+
@Test
204+
fun `extractFolderDate works at start of path`() {
205+
val result = GalleryAdapter.extractFolderDate("/2025/06/15/image.jpg")
206+
assertNotNull(result)
207+
208+
val cal = Calendar.getInstance().apply { timeInMillis = result!! }
209+
assertEquals(2025, cal.get(Calendar.YEAR))
210+
assertEquals(5, cal.get(Calendar.MONTH)) // June is 5
211+
assertEquals(15, cal.get(Calendar.DAY_OF_MONTH))
212+
}
213+
214+
@Test
215+
fun `extractFolderDate handles different years`() {
216+
val y2020 = GalleryAdapter.extractFolderDate("/Photos/2020/06/image.jpg")!!
217+
val y2025 = GalleryAdapter.extractFolderDate("/Photos/2025/06/image.jpg")!!
218+
219+
assert(y2025 > y2020) { "2025 should be after 2020" }
220+
}
111221
}

0 commit comments

Comments
 (0)