From 845c53231a5a573c7ed602ac050456f7f0c479f5 Mon Sep 17 00:00:00 2001 From: Wesley Moore Date: Sun, 7 Apr 2024 15:19:51 +1000 Subject: [PATCH] Print list of runs as CSV --- .gitignore | 1 + Cargo.lock | 114 +++++++++++++++ Cargo.toml | 10 ++ src/main.rs | 390 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 515 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..83217d8 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,114 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qhd" +version = "0.1.0" +dependencies = [ + "csv", + "quick-xml", +] + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5b7db8d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "qhd" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +csv = "1.3.0" +quick-xml = "0.31.0" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..85f962d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,390 @@ +use quick_xml::events::Event; +use quick_xml::reader::Reader; +use std::borrow::Cow; +use std::io::BufRead; +use std::path::PathBuf; +use std::process::ExitCode; +use std::{env, io}; + +/* + + */ +#[derive(Debug, Default)] +struct RunningWorkout { + duration: String, + duration_unit: String, + creation_date: String, + start_date: String, + end_date: String, + // + distance: String, + distance_unit: String, +} + +/// HKWorkoutActivityType +/// +/// +#[allow(unused)] +enum Activity { + // Individual sports + /// Shooting archery. + Archery, + /// Bowling. + Bowling, + /// Fencing. + Fencing, + /// Performing gymnastics. + Gymnastics, + /// Participating in track and field events, including shot put, javelin, pole vaulting, and related sports. + TrackAndField, + + // Team sports + /// Playing American football. + AmericanFootball, + /// Playing Australian football. + AustralianFootball, + /// Playing baseball. + Baseball, + /// Playing basketball. + Basketball, + /// Playing cricket. + Cricket, + /// Playing disc sports such as Ultimate and Disc Golf. + DiscSports, + /// Playing handball. + Handball, + /// Playing hockey, including ice hockey, field hockey, and related sports. + Hockey, + /// Playing lacrosse. + Lacrosse, + /// Playing rugby. + Rugby, + /// Playing soccer. + Soccer, + /// Playing softball. + Softball, + /// Playing volleyball. + Volleyball, + + // Exercise and fitness + /// Warm-up and therapeutic activities like foam rolling and stretching. + PreparationAndRecovery, + /// A flexibility workout. + Flexibility, + /// Low intensity stretching and mobility exercises following a more vigorous workout. + Cooldown, + /// Walking. + Walking, + /// Running and jogging. + Running, + /// A wheelchair workout at walking pace. + WheelchairWalkPace, + /// Wheelchair workout at running pace. + WheelchairRunPace, + /// Cycling. + Cycling, + /// Hand cycling. + HandCycling, + /// Core training. + CoreTraining, + /// Workouts on an elliptical machine. + Elliptical, + /// Strength training, primarily with free weights and body weight. + FunctionalStrengthTraining, + /// Strength training exercises primarily using machines or free weights. + TraditionalStrengthTraining, + /// Exercise that includes any mixture of cardio, strength, and/or flexibility training. + CrossTraining, + /// Workouts that mix a variety of cardio exercise machines or modalities. + MixedCardio, + /// High intensity interval training. + HighIntensityIntervalTraining, + /// Jumping rope. + JumpRope, + /// Workouts using a stair climbing machine. + StairClimbing, + /// Running, walking, or other drills using stairs (for example, in a stadium or inside a multilevel building). + Stairs, + /// Training using a step bench. + StepTraining, + /// Playing fitness-based video games. + FitnessGaming, + + // Studio activities + /// Barre workout. + Barre, + /// Cardiovascular dance workouts. + CardioDance, + /// Dancing with a partner or partners, such as swing, salsa, or folk dances. + SocialDance, + /// Practicing yoga. + Yoga, + /// Performing activities like walking meditation, Gyrotonic exercise, and Qigong. + MindAndBody, + /// A pilates workout. + Pilates, + + // Racket sports + /// Playing badminton. + Badminton, + /// Playing pickleball. + Pickleball, + /// Playing racquetball. + Racquetball, + /// Playing squash. + Squash, + /// Playing table tennis. + TableTennis, + /// Playing tennis. + Tennis, + + // Outdoor activities + /// Climbing. + Climbing, + /// Activities that involve riding a horse, including polo, horse racing, and horse riding. + EquestrianSports, + /// Fishing. + Fishing, + /// Playing golf. + Golf, + /// Hiking. + Hiking, + /// Hunting. + Hunting, + /// Play-based activities like tag, dodgeball, hopscotch, tetherball, and playing on a jungle gym. + Play, + + // Snow and ice sports + /// Cross country skiing. + CrossCountrySkiing, + /// Curling. + Curling, + /// Downhill skiing. + DownhillSkiing, + /// A variety of snow sports, including sledding, snowmobiling, or building a snowman. + SnowSports, + /// Snowboarding. + Snowboarding, + /// Skating activities, including ice skating, speed skating, inline skating, and skateboarding. + SkatingSports, + + // Water activities + /// Canoeing, kayaking, paddling an outrigger, paddling a stand-up paddle board, and related sports. + PaddleSports, + /// Rowing. + Rowing, + /// Sailing. + Sailing, + /// A variety of surf sports, including surfing, kite surfing, and wind surfing. + SurfingSports, + /// Swimming. + Swimming, + /// Aerobic exercise performed in shallow water. + WaterFitness, + /// Playing water polo. + WaterPolo, + /// A variety of water sports, including water skiing, wake boarding, and related activities. + WaterSports, + + // Martial arts + /// Boxing. + Boxing, + /// Kickboxing. + Kickboxing, + /// Practicing martial arts. + MartialArts, + /// Tai chi. + TaiChi, + /// Wrestling. + Wrestling, + + // Other activities + /// A workout that does not match any of the other workout activity types. + Other, + // Deprecated activity types + /// Dancing. + Dance, + /// Workouts inspired by dance, including Pilates, Barre, and Feldenkrais. + DanceInspiredTraining, + /// Performing any mix of cardio-focused exercises. + MixedMetabolicCardioTraining, + + // Multisport activities + /// Multisport activities like triathlons. + SwimBikeRun, + /// A constant for the transition time between activities in a multisport workout. + Transition, + + // Enumeration Cases + UnderwaterDiving, +} + +fn main() -> ExitCode { + let Some(export_path) = env::args_os().skip(1).next().map(PathBuf::from) else { + eprintln!("Usage: qhd path/to/export.xml"); + return ExitCode::FAILURE; + }; + + let mut reader = Reader::from_file(&export_path).expect("unable to read input path"); + reader.trim_text(true); + + match try_main(&mut reader) { + Ok(workouts) => { + // println!("{} workouts", workouts.len()); + match output_csv(&workouts) { + Ok(()) => ExitCode::SUCCESS, + Err(err) => { + eprintln!("CSV Error: {err}"); + ExitCode::FAILURE + } + } + } + Err(err) => { + eprintln!("Error: {err}"); + ExitCode::FAILURE + } + } +} + +fn try_main(reader: &mut Reader) -> Result, quick_xml::Error> +where + R: BufRead, +{ + let mut buf = Vec::new(); + + // Match with workoutActivityType=HKWorkoutActivityTypeRunning + // Find type=HKQuantityTypeIdentifierDistanceWalkingRunning + let mut workouts = Vec::new(); + let mut workout = None; + + // The `Reader` does not implement `Iterator` because it outputs borrowed data (`Cow`s) + loop { + match reader.read_event_into(&mut buf) { + Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e), + // exits the loop when reaching end of file + Ok(Event::Eof) => break, + + // Handle start element + Ok(Event::Start(e)) => match e.name().as_ref() { + b"Workout" => { + let Ok(Some(workout_activity_type)) = + e.try_get_attribute("workoutActivityType") + else { + buf.clear(); + continue; + }; + if workout_activity_type.value.as_ref() != b"HKWorkoutActivityTypeRunning" { + buf.clear(); + continue; + } + + let mut w = RunningWorkout::default(); + e.attributes().try_for_each(|a| { + let attr = a?; + match attr.key.as_ref() { + b"duration" => w.duration = string(attr.value), + b"durationUnit" => w.duration_unit = string(attr.value), + b"creationDate" => w.creation_date = string(attr.value), + b"startDate" => w.start_date = string(attr.value), + b"endDate" => w.end_date = string(attr.value), + _ => {} + } + Ok::<(), quick_xml::Error>(()) + })?; + workout = Some(w); + } + _ => {} + }, + + // Handle empty elements + Ok(Event::Empty(e)) => match e.name().as_ref() { + b"WorkoutStatistics" if workout.is_some() => { + let Ok(Some(stat_type)) = e.try_get_attribute("type") else { + buf.clear(); + continue; + }; + if stat_type.value.as_ref() != b"HKQuantityTypeIdentifierDistanceWalkingRunning" + { + buf.clear(); + continue; + } + + let (distance, unit) = e + .try_get_attribute("sum")? + .map(|attr| string(attr.value)) + .and_then(|sum| { + e.try_get_attribute("unit") + .ok()? + .map(|attr| (sum, string(attr.value))) + }) + .unwrap(); + let w = workout.as_mut().unwrap(); + w.distance = distance; + w.distance_unit = unit; + } + _ => {} + }, + + // Handle element end + Ok(Event::End(e)) if workout.is_some() => match e.name().as_ref() { + b"Workout" => workouts.push(workout.take().unwrap()), + _ => {} + }, + + // Ok(Event::Text(e)) => txt.push(e.unescape().unwrap().into_owned()), + + // There are several other `Event`s we do not consider here + _ => (), + } + // if we don't keep a borrow elsewhere, we can clear the buffer to keep memory usage low + buf.clear(); + } + + Ok(workouts) +} + +fn output_csv(workouts: &[RunningWorkout]) -> Result<(), csv::Error> { + let stdout = io::stdout().lock(); + let mut out = csv::Writer::from_writer(stdout); + + out.write_record(&[ + "start_date", + "end_date", + "creation_date", + "duration", + "duration_unit", + "distance", + "distance_unit", + ])?; + for w in workouts { + out.write_record(&[ + &w.start_date, + &w.end_date, + &w.creation_date, + &w.duration, + &w.duration_unit, + &w.distance, + &w.distance_unit, + ])?; + } + + Ok(()) +} + +fn string<'a>(data: Cow<'a, [u8]>) -> String { + String::from_utf8(data.into_owned()).unwrap() +}