feat: rewrite date logic (#2390)

* chore: remove unused fields

* chore: rewrite date logic

* chore: apply suggestions from Alex

* chore: add space in date format

* chore: re-add error handling in apply-changeset
This commit is contained in:
Richard Shiue
2023-05-09 22:37:20 +08:00
committed by GitHub
parent ba8cbe170c
commit 376f2d887d
21 changed files with 765 additions and 411 deletions

View File

@ -20,6 +20,9 @@ pub struct DateCellDataPB {
#[pb(index = 4)]
pub include_time: bool,
#[pb(index = 5)]
pub timezone_id: String,
}
#[derive(Clone, Debug, Default, ProtoBuf)]
@ -36,8 +39,8 @@ pub struct DateChangesetPB {
#[pb(index = 4, one_of)]
pub include_time: Option<bool>,
#[pb(index = 5)]
pub is_utc: bool,
#[pb(index = 5, one_of)]
pub timezone_id: Option<String>,
}
// Date
@ -48,9 +51,6 @@ pub struct DateTypeOptionPB {
#[pb(index = 2)]
pub time_format: TimeFormatPB,
#[pb(index = 3)]
pub include_time: bool,
}
impl From<DateTypeOption> for DateTypeOptionPB {
@ -58,7 +58,6 @@ impl From<DateTypeOption> for DateTypeOptionPB {
Self {
date_format: data.date_format.into(),
time_format: data.time_format.into(),
include_time: data.include_time,
}
}
}
@ -68,7 +67,6 @@ impl From<DateTypeOptionPB> for DateTypeOption {
Self {
date_format: data.date_format.into(),
time_format: data.time_format.into(),
include_time: data.include_time,
}
}
}

View File

@ -361,7 +361,7 @@ pub(crate) async fn update_cell_handler(
&params.field_id,
params.cell_changeset.clone(),
)
.await;
.await?;
Ok(())
}
@ -397,7 +397,7 @@ pub(crate) async fn insert_or_update_select_option_handler(
RowId::from(params.row_id),
params.items,
)
.await;
.await?;
Ok(())
}
@ -415,7 +415,7 @@ pub(crate) async fn delete_select_option_handler(
RowId::from(params.row_id),
params.items,
)
.await;
.await?;
Ok(())
}
@ -452,7 +452,7 @@ pub(crate) async fn update_select_option_cell_handler(
&params.cell_identifier.field_id,
changeset,
)
.await;
.await?;
Ok(())
}
@ -467,7 +467,7 @@ pub(crate) async fn update_date_cell_handler(
date: data.date,
time: data.time,
include_time: data.include_time,
is_utc: data.is_utc,
timezone_id: data.timezone_id,
};
let database_editor = manager.get_database(&cell_id.view_id).await?;
database_editor
@ -477,7 +477,7 @@ pub(crate) async fn update_date_cell_handler(
&cell_id.field_id,
cell_changeset,
)
.await;
.await?;
Ok(())
}

View File

@ -4,7 +4,7 @@ use std::fmt::Debug;
use collab_database::fields::Field;
use collab_database::rows::{get_field_type_from_cell, Cell, Cells};
use flowy_error::{ErrorCode, FlowyResult};
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use crate::entities::FieldType;
use crate::services::cell::{CellCache, CellProtobufBlob};
@ -63,16 +63,14 @@ pub fn apply_cell_data_changeset<C: ToCellChangeset>(
cell: Option<Cell>,
field: &Field,
cell_data_cache: Option<CellCache>,
) -> Cell {
) -> Result<Cell, FlowyError> {
let changeset = changeset.to_cell_changeset_str();
let field_type = FieldType::from(field.field_type);
match TypeOptionCellExt::new_with_cell_data_cache(field, cell_data_cache)
.get_type_option_cell_data_handler(&field_type)
{
None => Cell::default(),
Some(handler) => handler
.handle_cell_changeset(changeset, cell, field)
.unwrap_or_default(),
None => Ok(Cell::default()),
Some(handler) => Ok(handler.handle_cell_changeset(changeset, cell, field)?),
}
}
@ -196,11 +194,11 @@ pub fn stringify_cell_data(
}
pub fn insert_text_cell(s: String, field: &Field) -> Cell {
apply_cell_data_changeset(s, None, field, None)
apply_cell_data_changeset(s, None, field, None).unwrap()
}
pub fn insert_number_cell(num: i64, field: &Field) -> Cell {
apply_cell_data_changeset(num.to_string(), None, field, None)
apply_cell_data_changeset(num.to_string(), None, field, None).unwrap()
}
pub fn insert_url_cell(url: String, field: &Field) -> Cell {
@ -214,7 +212,7 @@ pub fn insert_url_cell(url: String, field: &Field) -> Cell {
_ => url,
};
apply_cell_data_changeset(url, None, field, None)
apply_cell_data_changeset(url, None, field, None).unwrap()
}
pub fn insert_checkbox_cell(is_check: bool, field: &Field) -> Cell {
@ -223,7 +221,7 @@ pub fn insert_checkbox_cell(is_check: bool, field: &Field) -> Cell {
} else {
UNCHECK.to_string()
};
apply_cell_data_changeset(s, None, field, None)
apply_cell_data_changeset(s, None, field, None).unwrap()
}
pub fn insert_date_cell(timestamp: i64, field: &Field) -> Cell {
@ -231,22 +229,22 @@ pub fn insert_date_cell(timestamp: i64, field: &Field) -> Cell {
date: Some(timestamp.to_string()),
time: None,
include_time: Some(false),
is_utc: true,
timezone_id: None,
})
.unwrap();
apply_cell_data_changeset(cell_data, None, field, None)
apply_cell_data_changeset(cell_data, None, field, None).unwrap()
}
pub fn insert_select_option_cell(option_ids: Vec<String>, field: &Field) -> Cell {
let changeset =
SelectOptionCellChangeset::from_insert_options(option_ids).to_cell_changeset_str();
apply_cell_data_changeset(changeset, None, field, None)
apply_cell_data_changeset(changeset, None, field, None).unwrap()
}
pub fn delete_select_option_cell(option_ids: Vec<String>, field: &Field) -> Cell {
let changeset =
SelectOptionCellChangeset::from_delete_options(option_ids).to_cell_changeset_str();
apply_cell_data_changeset(changeset, None, field, None)
apply_cell_data_changeset(changeset, None, field, None).unwrap()
}
/// Deserialize the String into cell specific data type.

View File

@ -466,20 +466,27 @@ impl DatabaseEditor {
row_id: RowId,
field_id: &str,
cell_changeset: T,
) -> Option<()>
) -> FlowyResult<()>
where
T: ToCellChangeset,
{
let (field, cell) = {
let database = self.database.lock();
let field = match database.fields.get_field(field_id) {
Some(field) => Ok(field),
None => {
let msg = format!("Field with id:{} not found", &field_id);
Err(FlowyError::internal().context(msg))
},
}?;
(
database.fields.get_field(field_id)?,
field,
database.get_cell(field_id, &row_id).map(|cell| cell.cell),
)
};
let cell_changeset = cell_changeset.to_cell_changeset_str();
let new_cell =
apply_cell_data_changeset(cell_changeset, cell, &field, Some(self.cell_cache.clone()));
apply_cell_data_changeset(cell_changeset, cell, &field, Some(self.cell_cache.clone()))?;
self.update_cell(view_id, row_id, field_id, new_cell).await
}
@ -489,7 +496,7 @@ impl DatabaseEditor {
row_id: RowId,
field_id: &str,
new_cell: Cell,
) -> Option<()> {
) -> FlowyResult<()> {
let old_row = { self.database.lock().get_row(&row_id) };
self.database.lock().update_row(&row_id, |row_update| {
row_update.update_cells(|cell_update| {
@ -516,7 +523,7 @@ impl DatabaseEditor {
field_id: field_id.to_string(),
}])
.await;
None
Ok(())
}
pub async fn create_select_option(
@ -536,9 +543,15 @@ impl DatabaseEditor {
field_id: &str,
row_id: RowId,
options: Vec<SelectOptionPB>,
) -> Option<()> {
let field = self.database.lock().fields.get_field(field_id)?;
let mut type_option = select_type_option_from_field(&field).ok()?;
) -> FlowyResult<()> {
let field = match self.database.lock().fields.get_field(field_id) {
Some(field) => Ok(field),
None => {
let msg = format!("Field with id:{} not found", &field_id);
Err(FlowyError::internal().context(msg))
},
}?;
let mut type_option = select_type_option_from_field(&field)?;
let cell_changeset = SelectOptionCellChangeset {
insert_option_ids: options.iter().map(|option| option.id.clone()).collect(),
..Default::default()
@ -557,8 +570,8 @@ impl DatabaseEditor {
self
.update_cell_with_changeset(view_id, row_id, field_id, cell_changeset)
.await;
None
.await?;
Ok(())
}
pub async fn delete_select_options(
@ -567,9 +580,15 @@ impl DatabaseEditor {
field_id: &str,
row_id: RowId,
options: Vec<SelectOptionPB>,
) -> Option<()> {
let field = self.database.lock().fields.get_field(field_id)?;
let mut type_option = select_type_option_from_field(&field).ok()?;
) -> FlowyResult<()> {
let field = match self.database.lock().fields.get_field(field_id) {
Some(field) => Ok(field),
None => {
let msg = format!("Field with id:{} not found", &field_id);
Err(FlowyError::internal().context(msg))
},
}?;
let mut type_option = select_type_option_from_field(&field)?;
let cell_changeset = SelectOptionCellChangeset {
delete_option_ids: options.iter().map(|option| option.id.clone()).collect(),
..Default::default()
@ -588,8 +607,8 @@ impl DatabaseEditor {
self
.update_cell_with_changeset(view_id, row_id, field_id, cell_changeset)
.await;
None
.await?;
Ok(())
}
pub async fn get_select_options(&self, row_id: RowId, field_id: &str) -> SelectOptionCellDataPB {

View File

@ -20,19 +20,74 @@ mod tests {
type_option.date_format = date_format;
match date_format {
DateFormat::Friendly => {
assert_date(&type_option, 1647251762, None, "Mar 14,2022", false, &field);
assert_date(
&type_option,
&field,
DateCellChangeset {
date: Some("1647251762".to_owned()),
time: None,
include_time: None,
timezone_id: None,
},
None,
"Mar 14, 2022",
);
},
DateFormat::US => {
assert_date(&type_option, 1647251762, None, "2022/03/14", false, &field);
assert_date(
&type_option,
&field,
DateCellChangeset {
date: Some("1647251762".to_owned()),
time: None,
include_time: None,
timezone_id: None,
},
None,
"2022/03/14",
);
},
DateFormat::ISO => {
assert_date(&type_option, 1647251762, None, "2022-03-14", false, &field);
assert_date(
&type_option,
&field,
DateCellChangeset {
date: Some("1647251762".to_owned()),
time: None,
include_time: None,
timezone_id: None,
},
None,
"2022-03-14",
);
},
DateFormat::Local => {
assert_date(&type_option, 1647251762, None, "03/14/2022", false, &field);
assert_date(
&type_option,
&field,
DateCellChangeset {
date: Some("1647251762".to_owned()),
time: None,
include_time: None,
timezone_id: None,
},
None,
"03/14/2022",
);
},
DateFormat::DayMonthYear => {
assert_date(&type_option, 1647251762, None, "14/03/2022", false, &field);
assert_date(
&type_option,
&field,
DateCellChangeset {
date: Some("1647251762".to_owned()),
time: None,
include_time: None,
timezone_id: None,
},
None,
"14/03/2022",
);
},
}
}
@ -41,8 +96,7 @@ mod tests {
#[test]
fn date_type_option_different_time_format_test() {
let mut type_option = DateTypeOption::default();
let field_type = FieldType::DateTime;
let field_rev = FieldBuilder::from_field_type(field_type).build();
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
for time_format in TimeFormat::iter() {
type_option.time_format = time_format;
@ -50,53 +104,77 @@ mod tests {
TimeFormat::TwentyFourHour => {
assert_date(
&type_option,
1653609600,
&field,
DateCellChangeset {
date: Some("1653609600".to_owned()),
time: None,
include_time: Some(true),
timezone_id: Some("Etc/UTC".to_owned()),
},
None,
"May 27,2022 00:00",
true,
&field_rev,
"May 27, 2022 00:00",
);
assert_date(
&type_option,
1653609600,
Some("9:00".to_owned()),
"May 27,2022 09:00",
true,
&field_rev,
&field,
DateCellChangeset {
date: Some("1653609600".to_owned()),
time: Some("9:00".to_owned()),
include_time: Some(true),
timezone_id: Some("Etc/UTC".to_owned()),
},
None,
"May 27, 2022 09:00",
);
assert_date(
&type_option,
1653609600,
Some("23:00".to_owned()),
"May 27,2022 23:00",
true,
&field_rev,
&field,
DateCellChangeset {
date: Some("1653609600".to_owned()),
time: Some("23:00".to_owned()),
include_time: Some(true),
timezone_id: Some("Etc/UTC".to_owned()),
},
None,
"May 27, 2022 23:00",
);
},
TimeFormat::TwelveHour => {
assert_date(
&type_option,
1653609600,
&field,
DateCellChangeset {
date: Some("1653609600".to_owned()),
time: None,
include_time: Some(true),
timezone_id: Some("Etc/UTC".to_owned()),
},
None,
"May 27,2022 12:00 AM",
true,
&field_rev,
"May 27, 2022 12:00 AM",
);
assert_date(
&type_option,
1653609600,
Some("9:00 AM".to_owned()),
"May 27,2022 09:00 AM",
true,
&field_rev,
&field,
DateCellChangeset {
date: Some("1653609600".to_owned()),
time: Some("9:00 AM".to_owned()),
include_time: Some(true),
timezone_id: None,
},
None,
"May 27, 2022 09:00 AM",
);
assert_date(
&type_option,
1653609600,
Some("11:23 pm".to_owned()),
"May 27,2022 11:23 PM",
true,
&field_rev,
&field,
DateCellChangeset {
date: Some("1653609600".to_owned()),
time: Some("11:23 pm".to_owned()),
include_time: Some(true),
timezone_id: Some("Etc/UTC".to_owned()),
},
None,
"May 27, 2022 11:23 PM",
);
},
}
@ -107,38 +185,58 @@ mod tests {
fn date_type_option_invalid_date_str_test() {
let type_option = DateTypeOption::default();
let field_type = FieldType::DateTime;
let field_rev = FieldBuilder::from_field_type(field_type).build();
assert_date(&type_option, "abc", None, "", false, &field_rev);
let field = FieldBuilder::from_field_type(field_type).build();
assert_date(
&type_option,
&field,
DateCellChangeset {
date: Some("abc".to_owned()),
time: None,
include_time: None,
timezone_id: None,
},
None,
"",
);
}
#[test]
#[should_panic]
fn date_type_option_invalid_include_time_str_test() {
let type_option = DateTypeOption::new();
let field_rev = FieldBuilder::from_field_type(FieldType::DateTime).build();
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
assert_date(
&type_option,
1653609600,
Some("1:".to_owned()),
"May 27,2022 01:00",
true,
&field_rev,
&field,
DateCellChangeset {
date: Some("1653609600".to_owned()),
time: Some("1:".to_owned()),
include_time: Some(true),
timezone_id: None,
},
None,
"May 27, 2022 01:00",
);
}
#[test]
#[should_panic]
fn date_type_option_empty_include_time_str_test() {
let type_option = DateTypeOption::new();
let field_rev = FieldBuilder::from_field_type(FieldType::DateTime).build();
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
assert_date(
&type_option,
1653609600,
Some("".to_owned()),
"May 27,2022 00:00",
true,
&field_rev,
&field,
DateCellChangeset {
date: Some("1653609600".to_owned()),
time: Some("".to_owned()),
include_time: Some(true),
timezone_id: None,
},
None,
"May 27, 2022 01:00",
);
}
@ -146,14 +244,18 @@ mod tests {
fn date_type_midnight_include_time_str_test() {
let type_option = DateTypeOption::new();
let field_type = FieldType::DateTime;
let field_rev = FieldBuilder::from_field_type(field_type).build();
let field = FieldBuilder::from_field_type(field_type).build();
assert_date(
&type_option,
1653609600,
Some("00:00".to_owned()),
"May 27,2022 00:00",
true,
&field_rev,
&field,
DateCellChangeset {
date: Some("1653609600".to_owned()),
time: Some("00:00".to_owned()),
include_time: Some(true),
timezone_id: None,
},
None,
"May 27, 2022 00:00",
);
}
@ -162,15 +264,18 @@ mod tests {
#[should_panic]
fn date_type_option_twelve_hours_include_time_str_in_twenty_four_hours_format() {
let type_option = DateTypeOption::new();
let field_rev = FieldBuilder::from_field_type(FieldType::DateTime).build();
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
assert_date(
&type_option,
1653609600,
Some("1:00 am".to_owned()),
"May 27,2022 01:00 AM",
true,
&field_rev,
&field,
DateCellChangeset {
date: Some("1653609600".to_owned()),
time: Some("1:00 am".to_owned()),
include_time: Some(true),
timezone_id: None,
},
None,
"May 27, 2022 01:00 AM",
);
}
@ -180,15 +285,19 @@ mod tests {
fn date_type_option_twenty_four_hours_include_time_str_in_twelve_hours_format() {
let mut type_option = DateTypeOption::new();
type_option.time_format = TimeFormat::TwelveHour;
let field_rev = FieldBuilder::from_field_type(FieldType::DateTime).build();
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
assert_date(
&type_option,
1653609600,
Some("20:00".to_owned()),
"May 27,2022 08:00 PM",
true,
&field_rev,
&field,
DateCellChangeset {
date: Some("1653609600".to_owned()),
time: Some("20:00".to_owned()),
include_time: Some(true),
timezone_id: None,
},
None,
"May 27, 2022 08:00 PM",
);
}
@ -218,25 +327,170 @@ mod tests {
assert_eq!(china_local_time, "03/14/2022 05:56 PM");
}
fn assert_date<T: ToString>(
/// The time component shouldn't remain the same since the timestamp is
/// completely overwritten. To achieve the desired result, also pass in the
/// time string along with the new timestamp.
#[test]
#[should_panic]
fn update_date_keep_time() {
let type_option = DateTypeOption::new();
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
let old_cell_data = initialize_date_cell(
&type_option,
DateCellChangeset {
date: Some("1700006400".to_owned()),
time: Some("08:00".to_owned()),
include_time: Some(true),
timezone_id: Some("Etc/UTC".to_owned()),
},
);
assert_date(
&type_option,
&field,
DateCellChangeset {
date: Some("1701302400".to_owned()),
time: None,
include_time: None,
timezone_id: None,
},
Some(old_cell_data),
"Nov 30, 2023 08:00",
);
}
#[test]
fn update_time_keep_date() {
let type_option = DateTypeOption::new();
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
let old_cell_data = initialize_date_cell(
&type_option,
DateCellChangeset {
date: Some("1700006400".to_owned()),
time: Some("08:00".to_owned()),
include_time: Some(true),
timezone_id: None,
},
);
assert_date(
&type_option,
&field,
DateCellChangeset {
date: None,
time: Some("14:00".to_owned()),
include_time: None,
timezone_id: None,
},
Some(old_cell_data),
"Nov 15, 2023 14:00",
);
}
#[test]
fn timezone_no_daylight_saving_time() {
let type_option = DateTypeOption::new();
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
assert_date(
&type_option,
&field,
DateCellChangeset {
date: Some("1672963200".to_owned()),
time: None,
include_time: Some(true),
timezone_id: Some("Asia/Tokyo".to_owned()),
},
None,
"Jan 06, 2023 09:00",
);
assert_date(
&type_option,
&field,
DateCellChangeset {
date: Some("1685404800".to_owned()),
time: None,
include_time: Some(true),
timezone_id: Some("Asia/Tokyo".to_owned()),
},
None,
"May 30, 2023 09:00",
);
}
#[test]
fn timezone_with_daylight_saving_time() {
let type_option = DateTypeOption::new();
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
assert_date(
&type_option,
&field,
DateCellChangeset {
date: Some("1672963200".to_owned()),
time: None,
include_time: Some(true),
timezone_id: Some("Europe/Paris".to_owned()),
},
None,
"Jan 06, 2023 01:00",
);
assert_date(
&type_option,
&field,
DateCellChangeset {
date: Some("1685404800".to_owned()),
time: None,
include_time: Some(true),
timezone_id: Some("Europe/Paris".to_owned()),
},
None,
"May 30, 2023 02:00",
);
}
#[test]
fn change_timezone() {
let type_option = DateTypeOption::new();
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
let old_cell_data = initialize_date_cell(
&type_option,
DateCellChangeset {
date: Some("1672963200".to_owned()),
time: None,
include_time: Some(true),
timezone_id: Some("Asia/China".to_owned()),
},
);
assert_date(
&type_option,
&field,
DateCellChangeset {
date: None,
time: None,
include_time: None,
timezone_id: Some("America/Los_Angeles".to_owned()),
},
Some(old_cell_data),
"Jan 05, 2023 16:00",
);
}
fn assert_date(
type_option: &DateTypeOption,
timestamp: T,
include_time_str: Option<String>,
expected_str: &str,
include_time: bool,
field: &Field,
changeset: DateCellChangeset,
old_cell_data: Option<Cell>,
expected_str: &str,
) {
let changeset = DateCellChangeset {
date: Some(timestamp.to_string()),
time: include_time_str,
is_utc: false,
include_time: Some(include_time),
};
let (cell, _) = type_option.apply_changeset(changeset, None).unwrap();
let (cell, cell_data) = type_option
.apply_changeset(changeset, old_cell_data)
.unwrap();
assert_eq!(
decode_cell_data(&cell, type_option, include_time, field),
expected_str.to_owned(),
decode_cell_data(&cell, type_option, cell_data.include_time, field),
expected_str,
);
}
@ -258,4 +512,9 @@ mod tests {
decoded_data.date
}
}
fn initialize_date_cell(type_option: &DateTypeOption, changeset: DateCellChangeset) -> Cell {
let (cell, _) = type_option.apply_changeset(changeset, None).unwrap();
cell
}
}

View File

@ -5,20 +5,21 @@ use crate::services::field::{
TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionTransform,
};
use chrono::format::strftime::StrftimeItems;
use chrono::NaiveDateTime;
use chrono::{DateTime, Local, NaiveDateTime, NaiveTime, Offset, TimeZone};
use chrono_tz::Tz;
use collab::core::any_map::AnyMapExtension;
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell;
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::str::FromStr;
// Date
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct DateTypeOption {
pub date_format: DateFormat,
pub time_format: TimeFormat,
pub include_time: bool,
}
impl TypeOption for DateTypeOption {
@ -30,7 +31,6 @@ impl TypeOption for DateTypeOption {
impl From<TypeOptionData> for DateTypeOption {
fn from(data: TypeOptionData) -> Self {
let include_time = data.get_bool_value("include_time").unwrap_or(false);
let date_format = data
.get_i64_value("data_format")
.map(DateFormat::from)
@ -42,7 +42,6 @@ impl From<TypeOptionData> for DateTypeOption {
Self {
date_format,
time_format,
include_time,
}
}
}
@ -52,7 +51,6 @@ impl From<DateTypeOption> for TypeOptionData {
TypeOptionDataBuilder::new()
.insert_i64_value("data_format", data.date_format.value())
.insert_i64_value("time_format", data.time_format.value())
.insert_bool_value("include_time", data.include_time)
.build()
}
}
@ -79,23 +77,26 @@ impl DateTypeOption {
fn today_desc_from_timestamp(&self, cell_data: DateCellData) -> DateCellDataPB {
let timestamp = cell_data.timestamp.unwrap_or_default();
let include_time = cell_data.include_time;
let timezone_id = cell_data.timezone_id;
let naive = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0);
if naive.is_none() {
return DateCellDataPB::default();
}
let naive = naive.unwrap();
if timestamp == 0 {
return DateCellDataPB::default();
}
let fmt = self.date_format.format_str();
let date = format!("{}", naive.format_with_items(StrftimeItems::new(fmt)));
let (date, time) = match cell_data.timestamp {
Some(timestamp) => {
let naive = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap();
let offset = match Tz::from_str(&timezone_id) {
Ok(timezone) => timezone.offset_from_utc_datetime(&naive).fix(),
Err(_) => Local::now().offset().clone(),
};
let time = if include_time {
let fmt = self.time_format.format_str();
format!("{}", naive.format_with_items(StrftimeItems::new(fmt)))
} else {
"".to_string()
let date_time = DateTime::<Local>::from_utc(naive, offset);
let fmt = self.date_format.format_str();
let date = format!("{}", date_time.format_with_items(StrftimeItems::new(fmt)));
let fmt = self.time_format.format_str();
let time = format!("{}", date_time.format_with_items(StrftimeItems::new(fmt)));
(date, time)
},
None => ("".to_owned(), "".to_owned()),
};
DateCellDataPB {
@ -103,32 +104,9 @@ impl DateTypeOption {
time,
include_time,
timestamp,
timezone_id,
}
}
fn timestamp_from_utc_with_time(
&self,
naive_date: &NaiveDateTime,
time_str: &Option<String>,
) -> FlowyResult<i64> {
if let Some(time_str) = time_str.as_ref() {
if !time_str.is_empty() {
let naive_time = chrono::NaiveTime::parse_from_str(time_str, self.time_format.format_str());
match naive_time {
Ok(naive_time) => {
return Ok(naive_date.date().and_time(naive_time).timestamp());
},
Err(_e) => {
let msg = format!("Parse {} failed", time_str);
return Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, &msg));
},
};
}
}
Ok(naive_date.timestamp())
}
}
impl TypeOptionTransform for DateTypeOption {}
@ -167,39 +145,129 @@ impl CellDataChangeset for DateTypeOption {
changeset: <Self as TypeOption>::CellChangeset,
cell: Option<Cell>,
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
let (timestamp, include_time) = match cell {
None => (None, false),
Some(cell) => {
let cell_data = DateCellData::from(&cell);
(cell_data.timestamp, cell_data.include_time)
// old date cell data
let (timestamp, include_time, timezone_id) = match cell {
None => (None, false, "".to_owned()),
Some(type_cell_data) => {
let cell_data = DateCellData::from(&type_cell_data);
(
cell_data.timestamp,
cell_data.include_time,
cell_data.timezone_id,
)
},
};
// update include_time and timezone_id if present
let include_time = match changeset.include_time {
None => include_time,
Some(include_time) => include_time,
};
let timestamp = match changeset.date_timestamp() {
None => timestamp,
Some(date_timestamp) => match (include_time, changeset.time) {
(true, Some(time)) => {
let time = Some(time.trim().to_uppercase());
let naive = NaiveDateTime::from_timestamp_opt(date_timestamp, 0);
if let Some(naive) = naive {
Some(self.timestamp_from_utc_with_time(&naive, &time)?)
} else {
Some(date_timestamp)
let timezone_id = match changeset.timezone_id {
None => timezone_id,
Some(ref timezone_id) => timezone_id.to_owned(),
};
let previous_datetime = match timestamp {
Some(timestamp) => NaiveDateTime::from_timestamp_opt(timestamp, 0),
None => None,
};
let new_date_timestamp = changeset.date_timestamp();
// parse the time string, which would be in the local timezone
let parsed_time = match (include_time, changeset.time) {
(true, Some(time_str)) => {
let result = NaiveTime::parse_from_str(&time_str, self.time_format.format_str());
match result {
Ok(time) => Ok(Some(time)),
Err(_e) => {
let msg = format!("Parse {} failed", time_str);
Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, &msg))
},
}
},
_ => Ok(None),
}?;
// Calculate the new timestamp, while considering the timezone. If a new
// timestamp is included in the changeset without an accompanying time
// string, the new timestamp will simply overwrite the old one. Meaning,
// in order to change the day without time in the frontend, the time string
// must also be passed.
let timestamp = match Tz::from_str(&timezone_id) {
Ok(timezone) => match parsed_time {
Some(time) => {
// a valid time is provided, so we replace the time component of old
// (or new timestamp if provided) with this.
let local_date = match new_date_timestamp {
Some(timestamp) => Some(
timezone
.from_utc_datetime(&NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap())
.date_naive(),
),
None => match previous_datetime {
Some(datetime) => Some(timezone.from_utc_datetime(&datetime).date_naive()),
None => None,
},
};
match local_date {
Some(date) => {
let local_datetime_naive = NaiveDateTime::new(date, time);
let local_datetime = timezone.from_local_datetime(&local_datetime_naive).unwrap();
Some(local_datetime.timestamp())
},
None => None,
}
},
_ => Some(date_timestamp),
None => match new_date_timestamp {
// no valid time, return old timestamp or new one if provided
Some(timestamp) => Some(timestamp),
None => timestamp,
},
},
Err(_) => match parsed_time {
// same logic as above, but using local time instead of timezone
Some(time) => {
let offset = Local::now().offset().clone();
let local_date = match new_date_timestamp {
Some(timestamp) => Some(
offset
.from_utc_datetime(&NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap())
.date_naive(),
),
None => match previous_datetime {
Some(datetime) => Some(offset.from_utc_datetime(&datetime).date_naive()),
None => None,
},
};
match local_date {
Some(date) => {
let local_datetime = NaiveDateTime::new(date, time);
let datetime = offset.from_local_datetime(&local_datetime).unwrap();
Some(datetime.timestamp())
},
None => None,
}
},
None => match new_date_timestamp {
Some(timestamp) => Some(timestamp),
None => timestamp,
},
},
};
let date_cell_data = DateCellData {
timestamp,
include_time,
timezone_id,
};
Ok((date_cell_data.clone().into(), date_cell_data))
Ok((Cell::from(date_cell_data.clone()), date_cell_data))
}
}

View File

@ -22,7 +22,7 @@ pub struct DateCellChangeset {
pub date: Option<String>,
pub time: Option<String>,
pub include_time: Option<bool>,
pub is_utc: bool,
pub timezone_id: Option<String>,
}
impl DateCellChangeset {
@ -57,27 +57,37 @@ impl ToCellChangeset for DateCellChangeset {
pub struct DateCellData {
pub timestamp: Option<i64>,
pub include_time: bool,
pub timezone_id: String,
}
impl From<&Cell> for DateCellData {
fn from(cell: &Cell) -> Self {
let timestamp = cell
.get_str_value(CELL_DATE)
.map(|data| data.parse::<i64>().unwrap_or_default());
.map(|data| data.parse::<i64>().ok())
.flatten();
let include_time = cell.get_bool_value("include_time").unwrap_or_default();
let timezone_id = cell.get_str_value("timezone_id").unwrap_or_default();
Self {
timestamp,
include_time,
timezone_id,
}
}
}
impl From<DateCellData> for Cell {
fn from(data: DateCellData) -> Self {
let timestamp_string = match data.timestamp {
Some(timestamp) => timestamp.to_string(),
None => "".to_owned(),
};
new_cell_builder(FieldType::DateTime)
.insert_str_value(CELL_DATE, data.timestamp.unwrap_or_default().to_string())
.insert_str_value(CELL_DATE, timestamp_string)
.insert_bool_value("include_time", data.include_time)
.insert_str_value("timezone_id", data.timezone_id)
.build()
}
}
@ -105,6 +115,7 @@ impl<'de> serde::Deserialize<'de> for DateCellData {
Ok(DateCellData {
timestamp: Some(value),
include_time: false,
timezone_id: "".to_owned(),
})
}
@ -121,6 +132,7 @@ impl<'de> serde::Deserialize<'de> for DateCellData {
{
let mut timestamp: Option<i64> = None;
let mut include_time: Option<bool> = None;
let mut timezone_id: Option<String> = None;
while let Some(key) = map.next_key()? {
match key {
@ -130,15 +142,20 @@ impl<'de> serde::Deserialize<'de> for DateCellData {
"include_time" => {
include_time = map.next_value()?;
},
"timezone_id" => {
timezone_id = map.next_value()?;
},
_ => {},
}
}
let include_time = include_time.unwrap_or(false);
let include_time = include_time.unwrap_or_default();
let timezone_id = timezone_id.unwrap_or_default();
Ok(DateCellData {
timestamp,
include_time,
timezone_id,
})
}
}
@ -203,7 +220,7 @@ impl DateFormat {
DateFormat::Local => "%m/%d/%Y",
DateFormat::US => "%Y/%m/%d",
DateFormat::ISO => "%Y-%m-%d",
DateFormat::Friendly => "%b %d,%Y",
DateFormat::Friendly => "%b %d, %Y",
DateFormat::DayMonthYear => "%d/%m/%Y",
}
}

View File

@ -21,17 +21,18 @@ mod tests {
&field_type,
&field
),
"Mar 14,2022"
"Mar 14, 2022"
);
let data = DateCellData {
timestamp: Some(1647251762),
include_time: true,
timezone_id: "".to_owned(),
};
assert_eq!(
stringify_cell_data(&data.into(), &FieldType::RichText, &field_type, &field),
"Mar 14,2022"
"Mar 14, 2022"
);
}