返回 last30days-skill
dates.py
根目录 / skills / last30days / scripts / lib / dates.py
1 """Date utilities for last30days skill."""
2
3 from datetime import datetime, timedelta, timezone
4 from typing import Optional, Tuple
5
6
7 def parse_as_of_date(as_of_date: Optional[str]) -> Optional[str]:
8 """Validate and normalize an --as-of date.
9
10 Args:
11 as_of_date: Date string in YYYY-MM-DD format.
12
13 Returns:
14 Normalized YYYY-MM-DD string, or None when no date was provided.
15
16 Raises:
17 ValueError: If the date is not in YYYY-MM-DD format.
18 """
19 if as_of_date is None:
20 return None
21
22 if not as_of_date.strip():
23 raise ValueError("--as-of must be in YYYY-MM-DD format.")
24
25 try:
26 parsed = datetime.strptime(as_of_date, "%Y-%m-%d").date()
27 except ValueError as exc:
28 raise ValueError(
29 f"Invalid --as-of date: {as_of_date}. Expected YYYY-MM-DD."
30 ) from exc
31
32 return parsed.isoformat()
33
34
35 def get_date_range(days: int = 30, as_of_date: Optional[str] = None) -> Tuple[str, str]:
36 """Get the date range for the last N days.
37
38 When as_of_date is provided, the range ends at that date instead of today.
39
40 Args:
41 days: Number of days to look back.
42 as_of_date: Optional end date in YYYY-MM-DD format.
43
44 Returns:
45 Tuple of (from_date, to_date) as YYYY-MM-DD strings.
46 """
47 normalized_as_of = parse_as_of_date(as_of_date)
48
49 if normalized_as_of:
50 to_date = datetime.strptime(normalized_as_of, "%Y-%m-%d").date()
51 else:
52 to_date = datetime.now(timezone.utc).date()
53
54 from_date = to_date - timedelta(days=days)
55 return from_date.isoformat(), to_date.isoformat()
56
57
58 def parse_date(date_str: Optional[str]) -> Optional[datetime]:
59 """Parse a date string in various formats.
60
61 Supports: YYYY-MM-DD, ISO 8601, Unix timestamp
62 """
63 if not date_str:
64 return None
65
66 # Try Unix timestamp (from Reddit)
67 try:
68 ts = float(date_str)
69 return datetime.fromtimestamp(ts, tz=timezone.utc)
70 except (ValueError, TypeError):
71 pass
72
73 # Try ISO formats
74 formats = [
75 "%Y-%m-%d",
76 "%Y-%m-%dT%H:%M:%S",
77 "%Y-%m-%dT%H:%M:%SZ",
78 "%Y-%m-%dT%H:%M:%S%z",
79 "%Y-%m-%dT%H:%M:%S.%f%z",
80 ]
81
82 for fmt in formats:
83 try:
84 dt = datetime.strptime(date_str, fmt)
85 if dt.tzinfo is not None:
86 return dt.astimezone(timezone.utc)
87 return dt.replace(tzinfo=timezone.utc)
88 except ValueError:
89 continue
90
91 return None
92
93
94 def timestamp_to_date(ts: Optional[float]) -> Optional[str]:
95 """Convert Unix timestamp to YYYY-MM-DD string."""
96 if ts is None:
97 return None
98 try:
99 dt = datetime.fromtimestamp(ts, tz=timezone.utc)
100 return dt.date().isoformat()
101 except (ValueError, TypeError, OSError):
102 return None
103
104
105 def get_date_confidence(date_str: Optional[str], from_date: str, to_date: str) -> str:
106 """Determine confidence level for a date.
107
108 Args:
109 date_str: The date to check (YYYY-MM-DD or None)
110 from_date: Start of valid range (YYYY-MM-DD)
111 to_date: End of valid range (YYYY-MM-DD)
112
113 Returns:
114 'high', 'med', or 'low'
115 """
116 if not date_str:
117 return 'low'
118
119 try:
120 dt = datetime.strptime(date_str, "%Y-%m-%d").date()
121 start = datetime.strptime(from_date, "%Y-%m-%d").date()
122 end = datetime.strptime(to_date, "%Y-%m-%d").date()
123
124 return 'high' if start <= dt <= end else 'low'
125 except ValueError:
126 return 'low'
127
128
129 def days_ago(date_str: Optional[str], reference_date: Optional[str] = None) -> Optional[int]:
130 """Calculate how many days before the reference date a date is.
131
132 If reference_date is None, use real today for backward compatibility.
133 Returns None if date is invalid or missing.
134 """
135 if not date_str:
136 return None
137
138 try:
139 dt = datetime.strptime(date_str, "%Y-%m-%d").date()
140 if reference_date:
141 today = datetime.strptime(reference_date, "%Y-%m-%d").date()
142 else:
143 today = datetime.now(timezone.utc).date()
144 delta = today - dt
145 return delta.days
146 except ValueError:
147 return None
148
149
150 def recency_score(
151 date_str: Optional[str],
152 max_days: int = 30,
153 reference_date: Optional[str] = None,
154 ) -> int:
155 """Calculate recency score (0-100).
156
157 0 days before reference_date = 100, max_days before reference_date = 0.
158 If reference_date is None, use real today for backward compatibility.
159 """
160 age = days_ago(date_str, reference_date=reference_date)
161 if age is None:
162 return 0
163
164 if age < 0:
165 return 100
166 if age >= max_days:
167 return 0
168
169 return int(100 * (1 - age / max_days))
170
170 lines PYTHON