Skip to content

Commit e5b5c19

Browse files
committed
Add timezone option for current day checks
1 parent a028da8 commit e5b5c19

7 files changed

Lines changed: 88 additions & 11 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ If the `theme` parameter is specified, any color customizations specified will b
137137
| `excludeDaysLabel` | Excluded days of the week text color | **hex code** without `#` or **css color** |
138138
| `date_format` | Date format pattern or empty for locale format | See note below on [📅 Date Formats](#-date-formats) |
139139
| `locale` | Locale for labels and numbers (Default: `en`) | ISO 639-1 code - See [🗪 Locales](#-locales) |
140+
| `timezone` | Timezone used to determine the current day | IANA timezone identifier, eg. `Asia/Kolkata` |
140141
| `short_numbers` | Use short numbers (e.g. 1.5k instead of 1,500) | `true` or `false` |
141142
| `type` | Output format (Default: `svg`) | Current options: `svg`, `png` or `json` |
142143
| `mode` | Streak mode (Default: `daily`) | `daily` (contribute daily) or `weekly` (contribute once per Sun-Sat week) |

src/cache.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
* user/options combinations that could produce the same concatenated string.
2020
*
2121
* @param string $user GitHub username
22-
* @param array $options Additional options that affect the stats (mode, exclude_days, starting_year)
22+
* @param array $options Additional options that affect the stats (mode, exclude_days, starting_year, timezone)
2323
* @return string Cache key (filename-safe)
2424
*/
2525
function getCacheKey(string $user, array $options = []): string
@@ -185,7 +185,7 @@ function clearExpiredCache(int $maxAge = CACHE_DURATION): int
185185
* Clear cache for a specific user
186186
*
187187
* Note: This function only clears the cache for the user with empty/default options.
188-
* Cache entries with non-empty options (starting_year, mode, exclude_days) will NOT
188+
* Cache entries with non-empty options (starting_year, mode, exclude_days, timezone) will NOT
189189
* be cleared. This is a limitation of the hash-based cache key system - we cannot
190190
* enumerate all possible option combinations without storing additional metadata.
191191
*

src/demo/index.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ function gtag() {
119119
<?php endforeach; ?>
120120
</select>
121121

122+
<label for="timezone">Timezone</label>
123+
<input class="param" type="text" id="timezone" name="timezone" placeholder="UTC" />
124+
122125
<label for="short-numbers">Short Numbers</label>
123126
<select class="param" id="short-numbers" name="short_numbers">
124127
<option>false</option>

src/demo/js/script.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const preview = {
1010
hide_border: "false",
1111
date_format: "",
1212
locale: "en",
13+
timezone: "",
1314
border_radius: "4.5",
1415
mode: "daily",
1516
type: "svg",

src/generator.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ function generateStreakStats(string $user, array $params = []): array
1919
$startingYear = isset($params["starting_year"]) ? intval($params["starting_year"]) : null;
2020
$mode = isset($params["mode"]) ? strval($params["mode"]) : null;
2121
$excludeDaysRaw = isset($params["exclude_days"]) ? strval($params["exclude_days"]) : "";
22+
$timezone = isset($params["timezone"]) ? strval($params["timezone"]) : "";
2223

2324
// Build cache options based on request parameters
2425
$cacheOptions = [
2526
"starting_year" => $startingYear,
2627
"mode" => $mode,
2728
"exclude_days" => $excludeDaysRaw,
29+
"timezone" => $timezone,
2830
];
2931

3032
// Check if cache is disabled
@@ -33,13 +35,13 @@ function generateStreakStats(string $user, array $params = []): array
3335
// Check for cached stats first (24 hour cache) unless cache is disabled
3436
$cachedStats = $useCache ? getCachedStats($user, $cacheOptions) : null;
3537

36-
if (!statsMissingOrStale($cachedStats)) {
38+
if (!statsMissingOrStale($cachedStats, $timezone)) {
3739
return $cachedStats;
3840
}
3941

4042
// Fetch fresh data from GitHub API
4143
$contributionGraphs = getContributionGraphs($user, $startingYear);
42-
$contributions = getContributionDates($contributionGraphs);
44+
$contributions = getContributionDates($contributionGraphs, $timezone);
4345

4446
if ($mode === "weekly") {
4547
$stats = getWeeklyContributionStats($contributions);
@@ -61,9 +63,10 @@ function generateStreakStats(string $user, array $params = []): array
6163
* Check if cached stats are missing or stale - Streak may be outdated if it ends before today
6264
*
6365
* @param array|null $cachedStats The cached stats to check
66+
* @param string $timezone Timezone identifier, or empty for the server default
6467
* @return bool True if the cached stats are stale and should be refreshed
6568
*/
66-
function statsMissingOrStale(?array $cachedStats): bool
69+
function statsMissingOrStale(?array $cachedStats, string $timezone = ""): bool
6770
{
6871
// If there are no cached stats, we need to refresh
6972
if ($cachedStats === null) {
@@ -87,9 +90,10 @@ function statsMissingOrStale(?array $cachedStats): bool
8790
$mode = $cachedStats["mode"] ?? "daily";
8891

8992
if ($mode === "weekly") {
90-
$startOfWeek = date("Y-m-d", strtotime("last Sunday"));
93+
$today = getCurrentDate($timezone);
94+
$startOfWeek = getPreviousSunday($today);
9195
return $currentStreakEnd < $startOfWeek;
9296
} else {
93-
return $currentStreakEnd < date("Y-m-d");
97+
return $currentStreakEnd < getCurrentDate($timezone);
9498
}
9599
}

src/stats.php

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -251,14 +251,16 @@ function getGraphQLCurlHandle(string $query, string $token): CurlHandle
251251
/**
252252
* Get an array of all dates with the number of contributions
253253
*
254-
* @param array<int,stdClass> $contributionCalendars List of GraphQL response objects by year
254+
* @param array<int,stdClass> $contributionGraphs List of GraphQL response objects by year
255+
* @param string $timezone Timezone identifier, or empty for the server default
256+
* @param DateTimeImmutable|null $now Current time override for tests
255257
* @return array<string,int> Y-M-D dates mapped to the number of contributions
256258
*/
257-
function getContributionDates(array $contributionGraphs): array
259+
function getContributionDates(array $contributionGraphs, string $timezone = "", ?DateTimeImmutable $now = null): array
258260
{
259261
$contributions = [];
260-
$today = date("Y-m-d");
261-
$tomorrow = date("Y-m-d", strtotime("tomorrow"));
262+
$today = getCurrentDate($timezone, $now);
263+
$tomorrow = date("Y-m-d", strtotime("$today +1 day"));
262264
// sort contribution calendars by year key
263265
ksort($contributionGraphs);
264266
foreach ($contributionGraphs as $graph) {
@@ -279,6 +281,25 @@ function getContributionDates(array $contributionGraphs): array
279281
return $contributions;
280282
}
281283

284+
/**
285+
* Get the current date for a requested timezone.
286+
*
287+
* @param string $timezone Timezone identifier, or empty for the server default
288+
* @param DateTimeImmutable|null $now Current time override for tests
289+
* @return string Current date in Y-m-d format
290+
*/
291+
function getCurrentDate(string $timezone = "", ?DateTimeImmutable $now = null): string
292+
{
293+
try {
294+
$dateTimezone = new DateTimeZone($timezone ?: date_default_timezone_get());
295+
} catch (Exception) {
296+
throw new InvalidArgumentException("Invalid timezone.", 400);
297+
}
298+
299+
$now = $now ?: new DateTimeImmutable("now");
300+
return $now->setTimezone($dateTimezone)->format("Y-m-d");
301+
}
302+
282303
/**
283304
* Normalize names of days of the week (eg. ["Sunday", " mon", "TUE"] -> ["Sun", "Mon", "Tue"])
284305
*

tests/StatsTest.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,53 @@ public function testFutureCommits(): void
320320
$this->assertEquals($expected, $stats);
321321
}
322322

323+
/**
324+
* Test contribution dates with timezone override
325+
*/
326+
public function testContributionDatesWithTimezone(): void
327+
{
328+
$now = new DateTimeImmutable("2026-01-01T01:00:00+00:00");
329+
$today = getCurrentDate("America/New_York", $now);
330+
$tomorrow = date("Y-m-d", strtotime("$today +1 day"));
331+
$inTwoDays = date("Y-m-d", strtotime("$today +2 days"));
332+
$contributionGraphs = [
333+
(object) [
334+
"data" => (object) [
335+
"user" => (object) [
336+
"contributionsCollection" => (object) [
337+
"contributionCalendar" => (object) [
338+
"weeks" => (object) [
339+
(object) [
340+
"contributionDays" => (object) [
341+
(object) [
342+
"contributionCount" => 1,
343+
"date" => $today,
344+
],
345+
(object) [
346+
"contributionCount" => 0,
347+
"date" => $tomorrow,
348+
],
349+
(object) [
350+
"contributionCount" => 1,
351+
"date" => $inTwoDays,
352+
],
353+
],
354+
],
355+
],
356+
],
357+
],
358+
],
359+
],
360+
],
361+
];
362+
363+
$contributions = getContributionDates($contributionGraphs, "America/New_York", $now);
364+
365+
$this->assertArrayHasKey($today, $contributions);
366+
$this->assertArrayNotHasKey($tomorrow, $contributions);
367+
$this->assertArrayNotHasKey($inTwoDays, $contributions);
368+
}
369+
323370
/**
324371
* Test weekly stats
325372
*/

0 commit comments

Comments
 (0)