Print list of runs as CSV
This commit is contained in:
commit
845c53231a
4 changed files with 515 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
114
Cargo.lock
generated
Normal file
114
Cargo.lock
generated
Normal 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
10
Cargo.toml
Normal 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
390
src/main.rs
Normal 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()
|
||||
}
|
Loading…
Reference in a new issue