1use crate::ExifTool;
6use crate::error::Result;
7use crate::types::TagId;
8use crate::write::WriteBuilder;
9use std::path::Path;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum DateShiftDirection {
14 Add,
16 Subtract,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum TimeUnit {
23 Years,
25 Months,
27 Days,
29 Hours,
31 Minutes,
33 Seconds,
35}
36
37impl TimeUnit {
38 #[allow(dead_code)]
40 fn abbreviation(&self) -> &'static str {
41 match self {
42 Self::Years => "y",
43 Self::Months => "m",
44 Self::Days => "d",
45 Self::Hours => "H",
46 Self::Minutes => "M",
47 Self::Seconds => "S",
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct DateTimeOffset {
55 years: i32,
56 months: i32,
57 days: i32,
58 hours: i32,
59 minutes: i32,
60 seconds: i32,
61}
62
63impl DateTimeOffset {
64 pub fn new() -> Self {
66 Self {
67 years: 0,
68 months: 0,
69 days: 0,
70 hours: 0,
71 minutes: 0,
72 seconds: 0,
73 }
74 }
75
76 pub fn years(mut self, value: i32) -> Self {
78 self.years = value;
79 self
80 }
81
82 pub fn months(mut self, value: i32) -> Self {
84 self.months = value;
85 self
86 }
87
88 pub fn days(mut self, value: i32) -> Self {
90 self.days = value;
91 self
92 }
93
94 pub fn hours(mut self, value: i32) -> Self {
96 self.hours = value;
97 self
98 }
99
100 pub fn minutes(mut self, value: i32) -> Self {
102 self.minutes = value;
103 self
104 }
105
106 pub fn seconds(mut self, value: i32) -> Self {
108 self.seconds = value;
109 self
110 }
111
112 pub fn format(&self) -> String {
115 let sign = if self.is_negative() { "-" } else { "+" };
116 format!(
117 "{}{}:{:02}:{:02} {:02}:{:02}:{:02}",
118 sign,
119 self.years.abs(),
120 self.months.abs(),
121 self.days.abs(),
122 self.hours.abs(),
123 self.minutes.abs(),
124 self.seconds.abs()
125 )
126 }
127
128 fn is_negative(&self) -> bool {
129 self.years < 0
130 || self.months < 0
131 || self.days < 0
132 || self.hours < 0
133 || self.minutes < 0
134 || self.seconds < 0
135 }
136}
137
138impl Default for DateTimeOffset {
139 fn default() -> Self {
140 Self::new()
141 }
142}
143
144pub trait AdvancedWriteOperations {
146 fn shift_datetime<P: AsRef<Path>>(&self, path: P, offset: DateTimeOffset) -> Result<()>;
164
165 fn shift_specific_datetime<P: AsRef<Path>>(
167 &self,
168 path: P,
169 tag: TagId,
170 offset: DateTimeOffset,
171 ) -> Result<()>;
172
173 fn numeric_operation<P: AsRef<Path>>(
175 &self,
176 path: P,
177 tag: TagId,
178 operation: NumericOperation,
179 ) -> Result<()>;
180
181 fn append_string<P: AsRef<Path>>(&self, path: P, tag: TagId, suffix: &str) -> Result<()>;
183
184 fn write_if<P: AsRef<Path>, F>(&self, path: P, condition: &str, builder_fn: F) -> Result<()>
186 where
187 F: FnOnce(WriteBuilder<'_>) -> WriteBuilder<'_>;
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub enum NumericOperation {
193 Add(i64),
195 Subtract(i64),
197 Multiply(i64),
199 Divide(i64),
201}
202
203impl NumericOperation {
204 fn operator(&self) -> &'static str {
205 match self {
206 Self::Add(_) => "+=",
207 Self::Subtract(_) => "-=",
208 Self::Multiply(_) => "*=",
209 Self::Divide(_) => "/=",
210 }
211 }
212
213 fn value(&self) -> i64 {
214 match self {
215 Self::Add(v) => *v,
216 Self::Subtract(v) => *v,
217 Self::Multiply(v) => *v,
218 Self::Divide(v) => *v,
219 }
220 }
221}
222
223impl AdvancedWriteOperations for ExifTool {
224 fn shift_datetime<P: AsRef<Path>>(&self, path: P, offset: DateTimeOffset) -> Result<()> {
225 let offset_str = offset.format();
226
227 self.write(path)
228 .arg(format!("-AllDates{}", offset_str))
229 .overwrite_original(true)
230 .execute()?;
231
232 Ok(())
233 }
234
235 fn shift_specific_datetime<P: AsRef<Path>>(
236 &self,
237 path: P,
238 tag: TagId,
239 offset: DateTimeOffset,
240 ) -> Result<()> {
241 let offset_str = offset.format();
242
243 self.write(path)
244 .arg(format!("-{}{}", tag.name(), offset_str))
245 .overwrite_original(true)
246 .execute()?;
247
248 Ok(())
249 }
250
251 fn numeric_operation<P: AsRef<Path>>(
252 &self,
253 path: P,
254 tag: TagId,
255 operation: NumericOperation,
256 ) -> Result<()> {
257 let op_str = format!(
258 "-{}{}{}",
259 tag.name(),
260 operation.operator(),
261 operation.value()
262 );
263
264 self.write(path)
265 .arg(op_str)
266 .overwrite_original(true)
267 .execute()?;
268
269 Ok(())
270 }
271
272 fn append_string<P: AsRef<Path>>(&self, path: P, tag: TagId, suffix: &str) -> Result<()> {
273 self.write(path)
274 .arg(format!("-{}+={}", tag.name(), suffix))
275 .overwrite_original(true)
276 .execute()?;
277
278 Ok(())
279 }
280
281 fn write_if<P: AsRef<Path>, F>(&self, path: P, _condition: &str, builder_fn: F) -> Result<()>
282 where
283 F: FnOnce(WriteBuilder<'_>) -> WriteBuilder<'_>,
284 {
285 let builder = self.write(path);
287 let builder = builder_fn(builder);
288 builder.execute().map(|_| ())
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn test_datetime_offset_format() {
298 let offset = DateTimeOffset::new().days(1).hours(2).minutes(30);
299
300 assert_eq!(offset.format(), "+0:00:01 02:30:00");
301 }
302
303 #[test]
304 fn test_datetime_offset_negative() {
305 let offset = DateTimeOffset::new().days(-1).hours(-2);
306
307 assert_eq!(offset.format(), "-0:00:01 02:00:00");
308 }
309
310 #[test]
311 fn test_numeric_operation() {
312 let op = NumericOperation::Add(5);
313 assert_eq!(op.operator(), "+=");
314 assert_eq!(op.value(), 5);
315
316 let op = NumericOperation::Multiply(2);
317 assert_eq!(op.operator(), "*=");
318 assert_eq!(op.value(), 2);
319 }
320}