Create Duration and Distance structures to handle rendering
These structures handle parsing and rendering of a Duration and a Distance, allowing that knowledge to be centralized and reused. Then I'm using those structures in a variety of places in order to ensure that the information gets rendered consistently.
This commit is contained in:
parent
4fd377a3f1
commit
7d5d639ed9
|
@ -17,10 +17,7 @@ You should have received a copy of the GNU General Public License along with Fit
|
||||||
// use chrono::NaiveDate;
|
// use chrono::NaiveDate;
|
||||||
// use ft_core::TraxRecord;
|
// use ft_core::TraxRecord;
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{
|
components::{steps_editor, time_distance_summary, weight_field, ActionGroup, Steps, Weight},
|
||||||
steps_editor, text_entry::distance_field, time_distance_summary, weight_field, ActionGroup,
|
|
||||||
Steps, Weight,
|
|
||||||
},
|
|
||||||
view_models::DayDetailViewModel,
|
view_models::DayDetailViewModel,
|
||||||
};
|
};
|
||||||
use emseries::{Record, RecordId};
|
use emseries::{Record, RecordId};
|
||||||
|
@ -419,7 +416,7 @@ where
|
||||||
biking_button.connect_clicked({
|
biking_button.connect_clicked({
|
||||||
let view_model = view_model.clone();
|
let view_model = view_model.clone();
|
||||||
move |_| {
|
move |_| {
|
||||||
let workout = view_model.new_record(RecordType::Walk);
|
let workout = view_model.new_record(RecordType::BikeRide);
|
||||||
add_row(workout);
|
add_row(workout);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,9 +27,7 @@ mod steps;
|
||||||
pub use steps::{steps_editor, Steps};
|
pub use steps::{steps_editor, Steps};
|
||||||
|
|
||||||
mod text_entry;
|
mod text_entry;
|
||||||
pub use text_entry::{
|
pub use text_entry::{distance_field, duration_field, time_field, weight_field, TextEntry};
|
||||||
distance_field, duration_field, time_field, weight_field, ParseError, TextEntry,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod time_distance;
|
mod time_distance;
|
||||||
pub use time_distance::{time_distance_detail, time_distance_summary};
|
pub use time_distance::{time_distance_detail, time_distance_summary};
|
||||||
|
|
|
@ -14,7 +14,7 @@ General Public License for more details.
|
||||||
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::components::{text_entry::OnUpdate, ParseError, TextEntry};
|
use crate::{components::TextEntry, types::ParseError};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
|
@ -14,13 +14,11 @@ General Public License for more details.
|
||||||
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use crate::types::{Distance, Duration, FormatOption, ParseError};
|
||||||
use dimensioned::si;
|
use dimensioned::si;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct ParseError;
|
|
||||||
|
|
||||||
pub type Renderer<T> = dyn Fn(&T) -> String;
|
pub type Renderer<T> = dyn Fn(&T) -> String;
|
||||||
pub type Parser<T> = dyn Fn(&str) -> Result<T, ParseError>;
|
pub type Parser<T> = dyn Fn(&str) -> Result<T, ParseError>;
|
||||||
pub type OnUpdate<T> = dyn Fn(Option<T>);
|
pub type OnUpdate<T> = dyn Fn(Option<T>);
|
||||||
|
@ -153,47 +151,28 @@ where
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn distance_field<OnUpdate>(
|
pub fn distance_field<OnUpdate>(value: Option<Distance>, on_update: OnUpdate) -> TextEntry<Distance>
|
||||||
value: Option<si::Meter<f64>>,
|
|
||||||
on_update: OnUpdate,
|
|
||||||
) -> TextEntry<si::Meter<f64>>
|
|
||||||
where
|
where
|
||||||
OnUpdate: Fn(Option<si::Meter<f64>>) + 'static,
|
OnUpdate: Fn(Option<Distance>) + 'static,
|
||||||
{
|
{
|
||||||
TextEntry::new(
|
TextEntry::new(
|
||||||
"0 km",
|
"0 km",
|
||||||
value,
|
value,
|
||||||
|v| format!("{} km", v.value_unsafe / 1000.),
|
|v| format!("{} km", v.value_unsafe / 1000.),
|
||||||
|s| {
|
Distance::parse,
|
||||||
let digits = take_digits(s.to_owned());
|
|
||||||
let value = digits.parse::<f64>().map_err(|_| ParseError)?;
|
|
||||||
println!("value: {}", value);
|
|
||||||
Ok(value * 1000. * si::M)
|
|
||||||
},
|
|
||||||
on_update,
|
on_update,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn duration_field<OnUpdate>(
|
pub fn duration_field<OnUpdate>(value: Option<Duration>, on_update: OnUpdate) -> TextEntry<Duration>
|
||||||
value: Option<si::Second<f64>>,
|
|
||||||
on_update: OnUpdate,
|
|
||||||
) -> TextEntry<si::Second<f64>>
|
|
||||||
where
|
where
|
||||||
OnUpdate: Fn(Option<si::Second<f64>>) + 'static,
|
OnUpdate: Fn(Option<Duration>) + 'static,
|
||||||
{
|
{
|
||||||
TextEntry::new(
|
TextEntry::new(
|
||||||
"0 minutes",
|
"0 minutes",
|
||||||
value,
|
value,
|
||||||
|v| format!("{} minutes", v.value_unsafe / 1000.),
|
|v| v.format(FormatOption::Abbreviated),
|
||||||
|s| {
|
|s| Duration::parse(s),
|
||||||
let digits = take_digits(s.to_owned());
|
|
||||||
let value = digits.parse::<f64>().map_err(|_| ParseError)?;
|
|
||||||
Ok(value * 60. * si::S)
|
|
||||||
},
|
|
||||||
on_update,
|
on_update,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn take_digits(s: String) -> String {
|
|
||||||
s.chars().take_while(|t| t.is_digit(10)).collect::<String>()
|
|
||||||
}
|
|
||||||
|
|
|
@ -17,28 +17,25 @@ You should have received a copy of the GNU General Public License along with Fit
|
||||||
// use crate::components::{EditView, ParseError, TextEntry};
|
// use crate::components::{EditView, ParseError, TextEntry};
|
||||||
// use chrono::{Local, NaiveDate};
|
// use chrono::{Local, NaiveDate};
|
||||||
// use dimensioned::si;
|
// use dimensioned::si;
|
||||||
use crate::components::{distance_field, duration_field, time_field};
|
use crate::{
|
||||||
|
components::{distance_field, duration_field, time_field},
|
||||||
|
types::{Distance, Duration, FormatOption},
|
||||||
|
};
|
||||||
use dimensioned::si;
|
use dimensioned::si;
|
||||||
use ft_core::{RecordType, TimeDistance};
|
use ft_core::{RecordType, TimeDistance};
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
pub fn time_distance_summary(
|
pub fn time_distance_summary(distance: Distance, duration: Duration) -> Option<gtk::Label> {
|
||||||
distance: si::Meter<f64>,
|
let text = match (*distance > si::M, *duration > si::S) {
|
||||||
duration: si::Second<f64>,
|
|
||||||
) -> Option<gtk::Label> {
|
|
||||||
let text = match (distance > si::M, duration > si::S) {
|
|
||||||
(true, true) => Some(format!(
|
(true, true) => Some(format!(
|
||||||
"{} kilometers of biking in {} minutes",
|
"{} of biking in {}",
|
||||||
distance.value_unsafe / 1000.,
|
distance.format(FormatOption::Full),
|
||||||
duration.value_unsafe / 60.
|
duration.format(FormatOption::Full)
|
||||||
)),
|
)),
|
||||||
(true, false) => Some(format!(
|
(true, false) => Some(format!("{} of biking", distance.format(FormatOption::Full))),
|
||||||
"{} kilometers of biking",
|
(false, true) => Some(format!("{} of biking", duration.format(FormatOption::Full))),
|
||||||
distance.value_unsafe / 1000.
|
|
||||||
)),
|
|
||||||
(false, true) => Some(format!("{} seconds of biking", duration.value_unsafe / 60.)),
|
|
||||||
(false, false) => None,
|
(false, false) => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -72,7 +69,7 @@ pub fn time_distance_detail(type_: ft_core::RecordType, record: ft_core::TimeDis
|
||||||
.label(
|
.label(
|
||||||
record
|
record
|
||||||
.distance
|
.distance
|
||||||
.map(|dist| format!("{}", dist))
|
.map(|dist| Distance::from(dist).format(FormatOption::Abbreviated))
|
||||||
.unwrap_or("".to_owned()),
|
.unwrap_or("".to_owned()),
|
||||||
)
|
)
|
||||||
.build(),
|
.build(),
|
||||||
|
@ -84,7 +81,7 @@ pub fn time_distance_detail(type_: ft_core::RecordType, record: ft_core::TimeDis
|
||||||
.label(
|
.label(
|
||||||
record
|
record
|
||||||
.duration
|
.duration
|
||||||
.map(|duration| format!("{}", duration))
|
.map(|duration| Duration::from(duration).format(FormatOption::Abbreviated))
|
||||||
.unwrap_or("".to_owned()),
|
.unwrap_or("".to_owned()),
|
||||||
)
|
)
|
||||||
.build(),
|
.build(),
|
||||||
|
@ -173,16 +170,16 @@ impl TimeDistanceEdit {
|
||||||
.widget(),
|
.widget(),
|
||||||
);
|
);
|
||||||
details_row.append(
|
details_row.append(
|
||||||
&distance_field(workout.distance, {
|
&distance_field(workout.distance.map(Distance::from), {
|
||||||
let s = s.clone();
|
let s = s.clone();
|
||||||
move |d| s.update_distance(d)
|
move |d| s.update_distance(d.map(|d| *d))
|
||||||
})
|
})
|
||||||
.widget(),
|
.widget(),
|
||||||
);
|
);
|
||||||
details_row.append(
|
details_row.append(
|
||||||
&duration_field(workout.duration, {
|
&duration_field(workout.duration.map(Duration::from), {
|
||||||
let s = s.clone();
|
let s = s.clone();
|
||||||
move |d| s.update_duration(d)
|
move |d| s.update_duration(d.map(|d| *d))
|
||||||
})
|
})
|
||||||
.widget(),
|
.widget(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
use chrono::{Duration, Local, NaiveDate};
|
use chrono::{Local, NaiveDate};
|
||||||
|
use dimensioned::si;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ParseError;
|
||||||
|
|
||||||
// This interval doesn't feel right, either. The idea that I have a specific interval type for just
|
// This interval doesn't feel right, either. The idea that I have a specific interval type for just
|
||||||
// NaiveDate is odd. This should be genericized, as should the iterator. Also, it shouldn't live
|
// NaiveDate is odd. This should be genericized, as should the iterator. Also, it shouldn't live
|
||||||
|
@ -12,7 +16,7 @@ pub struct DayInterval {
|
||||||
impl Default for DayInterval {
|
impl Default for DayInterval {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
start: (Local::now() - Duration::days(7)).date_naive(),
|
start: (Local::now() - chrono::Duration::days(7)).date_naive(),
|
||||||
end: Local::now().date_naive(),
|
end: Local::now().date_naive(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,10 +42,132 @@ impl Iterator for DayIterator {
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
if self.current <= self.end {
|
if self.current <= self.end {
|
||||||
let val = self.current;
|
let val = self.current;
|
||||||
self.current += Duration::days(1);
|
self.current += chrono::Duration::days(1);
|
||||||
Some(val)
|
Some(val)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum FormatOption {
|
||||||
|
Abbreviated,
|
||||||
|
Full,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
|
||||||
|
pub struct Distance {
|
||||||
|
value: si::Meter<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Distance {
|
||||||
|
pub fn format(&self, option: FormatOption) -> String {
|
||||||
|
match option {
|
||||||
|
FormatOption::Abbreviated => format!("{} km", self.value.value_unsafe / 1000.),
|
||||||
|
FormatOption::Full => format!("{} kilometers", self.value.value_unsafe / 1000.),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(s: &str) -> Result<Distance, ParseError> {
|
||||||
|
let digits = take_digits(s.to_owned());
|
||||||
|
let value = digits.parse::<f64>().map_err(|_| ParseError)?;
|
||||||
|
println!("value: {}", value);
|
||||||
|
Ok(Distance {
|
||||||
|
value: value * 1000. * si::M,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Add for Distance {
|
||||||
|
type Output = Distance;
|
||||||
|
fn add(self, rside: Self) -> Self::Output {
|
||||||
|
Self::Output::from(self.value + rside.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Sub for Distance {
|
||||||
|
type Output = Distance;
|
||||||
|
fn sub(self, rside: Self) -> Self::Output {
|
||||||
|
Self::Output::from(self.value - rside.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for Distance {
|
||||||
|
type Target = si::Meter<f64>;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<si::Meter<f64>> for Distance {
|
||||||
|
fn from(value: si::Meter<f64>) -> Self {
|
||||||
|
Self { value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
|
||||||
|
pub struct Duration {
|
||||||
|
value: si::Second<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Duration {
|
||||||
|
pub fn format(&self, option: FormatOption) -> String {
|
||||||
|
let (hours, minutes) = self.hours_and_minutes();
|
||||||
|
let (h, m) = match option {
|
||||||
|
FormatOption::Abbreviated => ("h", "m"),
|
||||||
|
FormatOption::Full => ("hours", "minutes"),
|
||||||
|
};
|
||||||
|
if hours > 0 {
|
||||||
|
format!("{}{} {}{}", hours, h, minutes, m)
|
||||||
|
} else {
|
||||||
|
format!("{}{}", minutes, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(s: &str) -> Result<Duration, ParseError> {
|
||||||
|
let digits = take_digits(s.to_owned());
|
||||||
|
let value = digits.parse::<f64>().map_err(|_| ParseError)?;
|
||||||
|
Ok(Duration {
|
||||||
|
value: value * 60. * si::S,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hours_and_minutes(&self) -> (i64, i64) {
|
||||||
|
let minutes: i64 = (self.value.value_unsafe / 60.).round() as i64;
|
||||||
|
let hours: i64 = (minutes / 60) as i64;
|
||||||
|
let minutes = minutes - (hours * 60);
|
||||||
|
(hours, minutes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Add for Duration {
|
||||||
|
type Output = Duration;
|
||||||
|
fn add(self, rside: Self) -> Self::Output {
|
||||||
|
Self::Output::from(self.value + rside.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Sub for Duration {
|
||||||
|
type Output = Duration;
|
||||||
|
fn sub(self, rside: Self) -> Self::Output {
|
||||||
|
Self::Output::from(self.value - rside.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for Duration {
|
||||||
|
type Target = si::Second<f64>;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<si::Second<f64>> for Duration {
|
||||||
|
fn from(value: si::Second<f64>) -> Self {
|
||||||
|
Self { value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_digits(s: String) -> String {
|
||||||
|
s.chars().take_while(|t| t.is_digit(10)).collect::<String>()
|
||||||
|
}
|
||||||
|
|
|
@ -14,7 +14,10 @@ General Public License for more details.
|
||||||
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::{
|
||||||
|
app::App,
|
||||||
|
types::{Distance, Duration},
|
||||||
|
};
|
||||||
use dimensioned::si;
|
use dimensioned::si;
|
||||||
use emseries::{Record, RecordId, Recordable};
|
use emseries::{Record, RecordId, Recordable};
|
||||||
use ft_core::{RecordType, TimeDistance, TraxRecord};
|
use ft_core::{RecordType, TimeDistance, TraxRecord};
|
||||||
|
@ -164,9 +167,9 @@ impl DayDetailViewModel {
|
||||||
*record = Some(new_record);
|
*record = Some(new_record);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn biking_summary(&self) -> (si::Meter<f64>, si::Second<f64>) {
|
pub fn biking_summary(&self) -> (Distance, Duration) {
|
||||||
self.records.read().unwrap().iter().fold(
|
self.records.read().unwrap().iter().fold(
|
||||||
(0. * si::M, 0. * si::S),
|
(Distance::default(), Duration::default()),
|
||||||
|(acc_distance, acc_duration), (_, record)| match record.data() {
|
|(acc_distance, acc_duration), (_, record)| match record.data() {
|
||||||
Some(Record {
|
Some(Record {
|
||||||
data:
|
data:
|
||||||
|
@ -176,11 +179,11 @@ impl DayDetailViewModel {
|
||||||
..
|
..
|
||||||
}) => (
|
}) => (
|
||||||
distance
|
distance
|
||||||
.map(|distance| acc_distance + distance)
|
.map(|distance| acc_distance + Distance::from(distance))
|
||||||
.unwrap_or(acc_distance),
|
.unwrap_or(acc_distance),
|
||||||
(duration
|
duration
|
||||||
.map(|duration| acc_duration + duration)
|
.map(|duration| acc_duration + Duration::from(duration))
|
||||||
.unwrap_or(acc_duration)),
|
.unwrap_or(acc_duration),
|
||||||
),
|
),
|
||||||
|
|
||||||
_ => (acc_distance, acc_duration),
|
_ => (acc_distance, acc_duration),
|
||||||
|
|
Loading…
Reference in New Issue