Print list of runs as CSV

This commit is contained in:
Wesley Moore 2024-04-07 15:19:51 +10:00
commit 845c53231a
No known key found for this signature in database
4 changed files with 515 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

114
Cargo.lock generated Normal file
View file

@ -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"

10
Cargo.toml Normal file
View file

@ -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"

390
src/main.rs Normal file
View file

@ -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};
/*
<!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()
}