qhd/src/main.rs

391 lines
12 KiB
Rust
Raw Permalink Normal View History

2024-04-07 05:19:51 +00:00
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()
}