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() }