Skip to content

Commit 3fef7ff

Browse files
dbejarano820claude
andcommitted
feat: cross-platform date handling with GNU/BSD/python3 support
Create scripts/lib/date-compat.sh with date_today, date_yesterday, date_to_epoch, date_days_ago, and date_now_iso helpers. All dates use UTC consistently. Closes DOJ-2430 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b96a272 commit 3fef7ff

File tree

3 files changed

+68
-9
lines changed

3 files changed

+68
-9
lines changed

scripts/lib/date-compat.sh

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env bash
2+
# Cross-platform date utilities for CodeSensei
3+
# Supports: GNU date (Linux), BSD date (macOS), python3 fallback
4+
5+
# Get today's date in YYYY-MM-DD format (UTC)
6+
date_today() {
7+
date -u '+%Y-%m-%d' 2>/dev/null || python3 -c "from datetime import datetime; print(datetime.utcnow().strftime('%Y-%m-%d'))"
8+
}
9+
10+
# Get yesterday's date in YYYY-MM-DD format (UTC)
11+
date_yesterday() {
12+
# Try GNU date first
13+
date -u -d 'yesterday' '+%Y-%m-%d' 2>/dev/null && return
14+
# Try BSD date (macOS)
15+
date -u -v-1d '+%Y-%m-%d' 2>/dev/null && return
16+
# Python fallback
17+
python3 -c "from datetime import datetime, timedelta; print((datetime.utcnow() - timedelta(days=1)).strftime('%Y-%m-%d'))" 2>/dev/null && return
18+
# Last resort: empty string
19+
echo ""
20+
}
21+
22+
# Convert date string (YYYY-MM-DD) to epoch seconds
23+
date_to_epoch() {
24+
local date_str="$1"
25+
# Try GNU date
26+
date -u -d "$date_str" '+%s' 2>/dev/null && return
27+
# Try BSD date (macOS)
28+
date -u -j -f '%Y-%m-%d' "$date_str" '+%s' 2>/dev/null && return
29+
# Python fallback
30+
python3 -c "from datetime import datetime; print(int(datetime.strptime('$date_str', '%Y-%m-%d').timestamp()))" 2>/dev/null && return
31+
echo "0"
32+
}
33+
34+
# Get date N days ago in YYYY-MM-DD format (UTC)
35+
date_days_ago() {
36+
local days="$1"
37+
# Try GNU date
38+
date -u -d "${days} days ago" '+%Y-%m-%d' 2>/dev/null && return
39+
# Try BSD date (macOS)
40+
date -u -v-${days}d '+%Y-%m-%d' 2>/dev/null && return
41+
# Python fallback
42+
python3 -c "from datetime import datetime, timedelta; print((datetime.utcnow() - timedelta(days=${days})).strftime('%Y-%m-%d'))" 2>/dev/null && return
43+
echo ""
44+
}
45+
46+
# Get current UTC timestamp in ISO format
47+
date_now_iso() {
48+
date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || python3 -c "from datetime import datetime; print(datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'))"
49+
}

scripts/quiz-selector.sh

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ PROFILE_FILE="$PROFILE_DIR/profile.json"
1919
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "$0")")}"
2020
QUIZ_BANK="$PLUGIN_ROOT/data/quiz-bank.json"
2121

22+
# Source cross-platform date helpers
23+
# shellcheck source=scripts/lib/date-compat.sh
24+
source "$PLUGIN_ROOT/scripts/lib/date-compat.sh"
25+
2226
# Default output if we can't determine anything
2327
DEFAULT_OUTPUT='{"mode":"dynamic","concept":null,"reason":"No profile data available","static_question":null,"belt":"white","quiz_format":"multiple_choice"}'
2428

@@ -40,8 +44,8 @@ SESSION_CONCEPTS=$(jq -c '.session_concepts // []' "$PROFILE_FILE")
4044
TOTAL_QUIZZES=$(jq -r '.quizzes.total // 0' "$PROFILE_FILE")
4145
CORRECT_QUIZZES=$(jq -r '.quizzes.correct // 0' "$PROFILE_FILE")
4246

43-
TODAY=$(date -u +%Y-%m-%d)
44-
NOW_EPOCH=$(date +%s)
47+
TODAY=$(date_today)
48+
NOW_EPOCH=$(date_to_epoch "$TODAY")
4549

4650
# Determine quiz format based on belt level
4751
# Orange Belt+ gets a mix of formats; lower belts get multiple choice
@@ -84,8 +88,8 @@ if [ "$QUIZ_HISTORY" != "[]" ]; then
8488

8589
# Calculate days since last wrong answer
8690
LAST_WRONG_DATE=$(echo "$LAST_WRONG" | cut -d'T' -f1)
87-
LAST_EPOCH=$(date -j -f "%Y-%m-%d" "$LAST_WRONG_DATE" +%s 2>/dev/null || date -d "$LAST_WRONG_DATE" +%s 2>/dev/null)
88-
if [ -n "$LAST_EPOCH" ]; then
91+
LAST_EPOCH=$(date_to_epoch "$LAST_WRONG_DATE")
92+
if [ -n "$LAST_EPOCH" ] && [ "$LAST_EPOCH" != "0" ]; then
8993
DAYS_SINCE=$(( (NOW_EPOCH - LAST_EPOCH) / 86400 ))
9094
else
9195
DAYS_SINCE=999

scripts/session-start.sh

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@
55
PROFILE_DIR="$HOME/.code-sensei"
66
PROFILE_FILE="$PROFILE_DIR/profile.json"
77
SESSION_LOG="$PROFILE_DIR/sessions.log"
8-
TODAY=$(date -u +%Y-%m-%d)
8+
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "$0")")}"
9+
10+
# Source cross-platform date helpers
11+
# shellcheck source=scripts/lib/date-compat.sh
12+
source "$PLUGIN_ROOT/scripts/lib/date-compat.sh"
13+
14+
TODAY=$(date_today)
915

1016
# Create profile directory if it doesn't exist
1117
mkdir -p "$PROFILE_DIR"
@@ -17,7 +23,7 @@ if [ ! -f "$PROFILE_FILE" ]; then
1723
"version": "1.0.0",
1824
"plugin": "code-sensei",
1925
"brand": "Dojo Coding",
20-
"created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
26+
"created_at": "$(date_now_iso)",
2127
"belt": "white",
2228
"xp": 0,
2329
"streak": {
@@ -45,7 +51,7 @@ if [ ! -f "$PROFILE_FILE" ]; then
4551
"id": "first-session",
4652
"name": "First Steps",
4753
"description": "Started your first CodeSensei session",
48-
"earned_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
54+
"earned_at": "$(date_now_iso)"
4955
}
5056
],
5157
"preferences": {
@@ -73,7 +79,7 @@ if command -v jq &> /dev/null; then
7379
NEW_STREAK=$CURRENT_STREAK
7480
elif [ -n "$LAST_SESSION" ]; then
7581
# Check if last session was yesterday
76-
YESTERDAY=$(date -u -d "yesterday" +%Y-%m-%d 2>/dev/null || date -u -v-1d +%Y-%m-%d 2>/dev/null)
82+
YESTERDAY=$(date_yesterday)
7783
if [ "$LAST_SESSION" = "$YESTERDAY" ]; then
7884
NEW_STREAK=$((CURRENT_STREAK + 1))
7985
else
@@ -107,7 +113,7 @@ if command -v jq &> /dev/null; then
107113
echo "$UPDATED" > "$PROFILE_FILE"
108114

109115
# Log session
110-
echo "$TODAY $(date -u +%H:%M:%S) session_start" >> "$SESSION_LOG"
116+
echo "$(date_now_iso) session_start" >> "$SESSION_LOG"
111117

112118
# Show streak info if notable
113119
BELT=$(jq -r '.belt // "white"' "$PROFILE_FILE")

0 commit comments

Comments
 (0)