391 lines
12 KiB
Rust
391 lines
12 KiB
Rust
|
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};
|
||
|
|
||
|
/*
|
||
|
<!ATTLIST Workout
|
||
|
workoutActivityType CDATA #REQUIRED
|
||
|
duration CDATA #IMPLIED
|
||
|
durationUnit CDATA #IMPLIED
|
||
|
totalDistance CDATA #IMPLIED
|
||
|
totalDistanceUnit CDATA #IMPLIED
|
||
|
totalEnergyBurned CDATA #IMPLIED
|
||
|
totalEnergyBurnedUnit CDATA #IMPLIED
|
||
|
sourceName CDATA #REQUIRED
|
||
|
sourceVersion CDATA #IMPLIED
|
||
|
device CDATA #IMPLIED
|
||
|
creationDate CDATA #IMPLIED
|
||
|
startDate CDATA #REQUIRED
|
||
|
endDate CDATA #REQUIRED
|
||
|
>
|
||
|
*/
|
||
|
#[derive(Debug, Default)]
|
||
|
struct RunningWorkout {
|
||
|
duration: String,
|
||
|
duration_unit: String,
|
||
|
creation_date: String,
|
||
|
start_date: String,
|
||
|
end_date: String,
|
||
|
// <WorkoutStatistics type="HKQuantityTypeIdentifierDistanceWalkingRunning" startDate="2024-03-30 17:00:48 +1000" endDate="2024-03-30 17:25:28 +1000" sum="4.10642" unit="km"/>
|
||
|
distance: String,
|
||
|
distance_unit: String,
|
||
|
}
|
||
|
|
||
|
/// HKWorkoutActivityType
|
||
|
///
|
||
|
/// <https://developer.apple.com/documentation/healthkit/hkworkoutactivitytype?language=objc>
|
||
|
#[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<R>(reader: &mut Reader<R>) -> Result<Vec<RunningWorkout>, quick_xml::Error>
|
||
|
where
|
||
|
R: BufRead,
|
||
|
{
|
||
|
let mut buf = Vec::new();
|
||
|
|
||
|
// Match <Workout> with workoutActivityType=HKWorkoutActivityTypeRunning
|
||
|
// Find <WorkoutStatistics> 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()
|
||
|
}
|