artichoke_backend/extn/core/time/
offset.rs1use crate::convert::{implicitly_convert_to_int, implicitly_convert_to_string};
4use crate::extn::core::symbol::Symbol;
5use crate::extn::core::time::Offset;
6use crate::extn::prelude::*;
7
8const MAX_FLOAT_OFFSET: f64 = i32::MAX as f64;
9const MIN_FLOAT_OFFSET: f64 = i32::MIN as f64;
10
11impl TryConvertMut<Value, Option<Offset>> for Artichoke {
12 type Error = Error;
13
14 fn try_convert_mut(&mut self, options: Value) -> Result<Option<Offset>, Self::Error> {
15 let hash: Vec<(Value, Value)> = self.try_convert_mut(options)?;
16
17 if hash.is_empty() {
26 return Ok(None);
27 }
28
29 for &(mut key, _) in &hash {
45 let k = unsafe { Symbol::unbox_from_value(&mut key, self)? }.bytes(self);
46 if k != b"in" {
47 let mut message = b"unknown keyword: ".to_vec();
48 message.extend_from_slice(k);
49 Err(ArgumentError::from(message))?;
50 }
51 }
52
53 let mut in_value = hash.first().expect("Only the `in` parameter should be available").1;
56
57 match in_value.ruby_type() {
58 Ruby::String => {
59 let offset_str = unsafe { implicitly_convert_to_string(self, &mut in_value) }?;
60
61 let offset = Offset::try_from(offset_str).map_err(|_| {
62 let mut message =
63 br#"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: "#.to_vec();
64 message.extend_from_slice(offset_str);
65 ArgumentError::from(message)
66 })?;
67
68 Ok(Some(offset))
69 }
70 Ruby::Float => {
71 let offset_seconds: f64 = self.try_convert(in_value)?;
75
76 if (MIN_FLOAT_OFFSET..=MAX_FLOAT_OFFSET).contains(&offset_seconds) {
77 #[expect(
78 clippy::cast_possible_truncation,
79 reason = "bounds check ensures float is within i32 range"
80 )]
81 Ok(Some(Offset::try_from(offset_seconds as i32)?))
82 } else {
83 Err(ArgumentError::with_message("utc_offset out of range").into())
84 }
85 }
86 _ => {
87 let offset_seconds = implicitly_convert_to_int(self, in_value).and_then(|seconds| {
88 i32::try_from(seconds).map_err(|_| ArgumentError::with_message("utc_offset out of range").into())
89 })?;
90
91 Ok(Some(Offset::try_from(offset_seconds)?))
92 }
93 }
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use bstr::ByteSlice;
100
101 use crate::extn::core::time::Offset;
102 use crate::test::prelude::*;
103
104 #[test]
105 fn no_options_does_not_raise() {
106 let mut interp = interpreter();
107
108 let options = interp.eval(b"{}").unwrap();
109
110 let offset: Option<Offset> = interp.try_convert_mut(options).unwrap();
111 assert_eq!(offset, None);
112 }
113
114 #[test]
115 fn raises_on_keys_except_in() {
116 let mut interp = interpreter();
117
118 let options = interp.eval(b"{ foo: 'bar' }").unwrap();
119
120 let result: Result<Option<Offset>, Error> = interp.try_convert_mut(options);
121 let error = result.unwrap_err();
122
123 assert_eq!(error.name(), "ArgumentError");
124 assert_eq!(error.message().as_bstr(), b"unknown keyword: foo".as_bstr());
125 }
126
127 #[test]
128 fn raises_on_invalid_timezone_string() {
129 let mut interp = interpreter();
130
131 let options = interp.eval(b"{ in: 'J' }").unwrap();
132
133 let result: Result<Option<Offset>, Error> = interp.try_convert_mut(options);
134 let error = result.unwrap_err();
135
136 assert_eq!(error.name(), "ArgumentError");
137 assert_eq!(
138 error.message().as_bstr(),
139 br#"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: J"#.as_bstr()
140 );
141 }
142
143 #[test]
144 fn provides_an_int_based_offset() {
145 let mut interp = interpreter();
146
147 let options = interp.eval(b"{ in: 3600 }").unwrap();
148
149 let result: Option<Offset> = interp.try_convert_mut(options).unwrap();
150 assert_eq!(result.unwrap(), Offset::fixed(3600).unwrap());
151 }
152
153 #[test]
154 fn provides_a_float_based_offset() {
155 let mut interp = interpreter();
156
157 let options = interp.eval(b"{ in: 3600.0 }").unwrap();
158
159 let result: Option<Offset> = interp.try_convert_mut(options).unwrap();
160 assert_eq!(result.unwrap(), Offset::fixed(3600).unwrap());
161 }
162
163 #[test]
164 fn provides_a_string_based_offset() {
165 let mut interp = interpreter();
166
167 let options = interp.eval(b"{ in: 'A' }").unwrap();
168
169 let result: Option<Offset> = interp.try_convert_mut(options).unwrap();
170 assert_eq!(result.unwrap(), Offset::fixed(3600).unwrap());
171 }
172
173 #[test]
174 fn raises_on_float_out_of_range() {
175 let mut interp = interpreter();
176
177 let options = interp.eval(b"{ in: -2_147_483_649.00 }").unwrap();
179
180 let result: Result<Option<Offset>, Error> = interp.try_convert_mut(options);
181 let error = result.unwrap_err();
182
183 assert_eq!(error.message(), b"utc_offset out of range".as_slice());
184 assert_eq!(error.name(), "ArgumentError");
185
186 let options = interp.eval(b"{ in: 2_147_483_648.00 }").unwrap();
188
189 let result: Result<Option<Offset>, Error> = interp.try_convert_mut(options);
190 let error = result.unwrap_err();
191
192 assert_eq!(error.message().as_bstr(), b"utc_offset out of range".as_bstr());
193 assert_eq!(error.name(), "ArgumentError");
194 }
195
196 #[test]
197 fn raises_on_int_out_of_range() {
198 let mut interp = interpreter();
199
200 let options = interp.eval(b"{ in: -2_147_483_649 }").unwrap();
202
203 let result: Result<Option<Offset>, Error> = interp.try_convert_mut(options);
204 let error = result.unwrap_err();
205
206 assert_eq!(error.message().as_bstr(), b"utc_offset out of range".as_bstr());
207 assert_eq!(error.name(), "ArgumentError");
208
209 let options = interp.eval(b"{ in: 2_147_483_648 }").unwrap();
211
212 let result: Result<Option<Offset>, Error> = interp.try_convert_mut(options);
213 let error = result.unwrap_err();
214
215 assert_eq!(error.message().as_bstr(), b"utc_offset out of range".as_bstr());
216 assert_eq!(error.name(), "ArgumentError");
217 }
218}