1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
/*!
# Utc2k: Local Offsets
*/

use crate::{
	FmtUtc2k,
	Utc2k,
};
use once_cell::sync::Lazy;
use std::ops::Neg;
use tz::timezone::{
	LocalTimeType,
	TimeZone,
};



#[cfg_attr(feature = "docsrs", doc(cfg(feature = "local")))]
#[derive(Debug, Clone, Copy, Default, Eq, Hash, PartialEq)]
/// # Local Offset.
///
/// This struct attempts to determine the appropriate UTC offset for the local
/// timezone in a thread-safe manner, but **only for unix systems**.
///
/// Instantiation will never fail, though.
///
/// If the platform isn't supported or no offset can be determined, the
/// "offset" will simply be zero (i.e. as if it were UTC).
///
/// ## Examples
///
/// To obtain the _current_ local offset, use [`LocalOffset::now`].
///
/// To obtain the local offset for a specific unix timestamp, use its `From<u32>`
/// implementation instead.
///
/// ```
/// use utc2k::LocalOffset;
///
/// // The current offset.
/// let now = LocalOffset::now();
///
/// // The offset for a specific time.
/// let then = LocalOffset::from(946_684_800_u32);
/// ```
///
/// If all you want is the offset, you can grab that by calling
/// [`LocalOffset::offset`] afterward.
///
/// This struct can, however, also be used to _trick_ [`FmtUtc2k`] and [`Utc2k`]
/// into representing _local_ rather than UTC datetimes by using their
/// `From<LocalOffset>` implementations, or the [`FmtUtc2k::now_local`]/[`Utc2k::now_local`]
/// shorthands.
///
/// If you do this, however, it is worth noting that comparing tricked and
/// untricked objects makes no logical sense, so you shouldn't mix-and-match if
/// you need to test for equality or ordering.
///
/// Additionally, you should avoid localizing a datetime if you plan on using the
/// `to_rfc2822` or `to_rfc3339` formatting helpers. They always assume they're
/// representing a UTC timestamp, so will add the wrong suffix to their output
/// if your local offset is non-zero.
///
/// Other than that, the trick works perfectly well. ;)
///
/// ```
/// use utc2k::{LocalOffset, Utc2k};
///
/// let now_utc = Utc2k::now();
///
/// // These two are equivalent.
/// let now_local = Utc2k::from(LocalOffset::now());
/// let now_local = Utc2k::now_local();
/// ```
///
/// If you need to convert from a local timestamp into a UTC one, you can
/// leverage [`std::ops::Neg`] to invert the offset, like:
///
/// ```
/// use utc2k::{LocalOffset, Utc2k};
///
/// let offset = LocalOffset::from(946_684_800_u32);
/// let utc = Utc2k::from(-offset);
/// ```
pub struct LocalOffset {
	unixtime: u32,
	offset: i32,
}

impl From<u32> for LocalOffset {
	#[inline]
	fn from(unixtime: u32) -> Self {
		let offset = offset(unixtime);
		Self { unixtime, offset }
	}
}

impl From<Utc2k> for LocalOffset {
	#[inline]
	/// # From `Utc2k`
	///
	/// Warning: this should only be used for `Utc2k` instances holding honest
	/// UTC datetimes. If you call this on a tricked/local instance, the offset
	/// will get applied twice!
	fn from(src: Utc2k) -> Self { Self::from(src.unixtime()) }
}

impl Neg for LocalOffset {
	type Output = Self;

	fn neg(self) -> Self::Output {
		if self.offset == i32::MIN {
			Self {
				unixtime: self.unixtime,
				offset: i32::MAX,
			}
		}
		else {
			Self {
				unixtime: self.unixtime,
				offset: self.offset.wrapping_neg(),
			}
		}
	}
}

impl LocalOffset {
	#[inline]
	#[must_use]
	/// # Now.
	///
	/// Return the current, local offset. If no offset can be determined, UTC
	/// will be assumed.
	///
	/// ## Examples
	///
	/// ```
	/// let now = utc2k::LocalOffset::now();
	/// ```
	pub fn now() -> Self { Self::from(crate::unixtime()) }

	#[must_use]
	/// # Local Timestamp.
	///
	/// Return the sum of the unix timestamp and the offset.
	pub const fn localtime(self) -> u32 {
		if self.offset < 0 {
			self.unixtime.saturating_sub(self.offset.abs() as u32)
		}
		else {
			self.unixtime.saturating_add(self.offset.abs() as u32)
		}
	}

	#[inline]
	#[must_use]
	/// # Offset.
	///
	/// Return the local offset (in seconds). Zero is returned if there is no
	/// offset, or no offset could be determined.
	pub const fn offset(self) -> i32 { self.offset }

	#[inline]
	#[must_use]
	/// # Unixtime.
	///
	/// Return the unix timestamp this instance applies to (i.e. the value it
	/// was seeded with).
	///
	/// ## Examples
	///
	/// ```
	/// use utc2k::LocalOffset;
	///
	/// let offset = LocalOffset::from(946_684_800_u32);
	/// assert_eq!(offset.unixtime(), 946_684_800_u32);
	/// ```
	pub const fn unixtime(self) -> u32 { self.unixtime }
}

impl From<LocalOffset> for i32 {
	fn from(src: LocalOffset) -> Self { src.offset }
}

impl From<LocalOffset> for FmtUtc2k {
	fn from(src: LocalOffset) -> Self { Self::from(Utc2k::from(src)) }
}

impl From<LocalOffset> for Utc2k {
	#[inline]
	fn from(src: LocalOffset) -> Self { Self::from(src.localtime()) }
}



/// # Offset From Unixtime.
///
/// The local timezone details are cached on the first run; subsequent method
/// calls will perform much faster.
fn offset(now: u32) -> i32 {
	static TZ: Lazy<TimeZone> = Lazy::new(||
		TimeZone::local().unwrap_or_else(|_| TimeZone::utc())
	);

	TZ.find_local_time_type(i64::from(now)).map_or(0, LocalTimeType::ut_offset)
}



#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn neg() {
		let off = LocalOffset::now();
		let off2 = -off;
		assert_eq!(off, -off2); // We should be back to the original.
	}

	#[test]
	fn now() {
		// Unless we're one second away from a DST-type change, the offsets
		// should match!
		let now = crate::unixtime();
		assert_eq!(LocalOffset::now().offset, LocalOffset::from(now).offset);
	}
}