use std::{
io::Write,
process::{Command, Stdio},
};
use fp_index::{
DateOffset, DateRangeError, Index, IndexLabel, apply_date_offset, bdate_range, date_range,
infer_freq_from_timestamps,
};
use fp_types::Timedelta;
use serde::Deserialize;
use super::HarnessConfig;
#[derive(Debug, Clone, Copy)]
struct DateRangeCase<'a> {
case_id: &'a str,
start: Option<&'a str>,
end: Option<&'a str>,
periods: Option<usize>,
pandas_freq: &'a str,
rust_freq_nanos: i64,
name: Option<&'a str>,
}
#[derive(Debug, Clone, Copy)]
struct BusinessDateRangeCase<'a> {
case_id: &'a str,
start: Option<&'a str>,
end: Option<&'a str>,
periods: Option<usize>,
name: Option<&'a str>,
}
#[derive(Debug, Clone, Copy)]
struct PandasRangeOracleCase<'a> {
case_id: &'a str,
pandas_function: &'a str,
start: Option<&'a str>,
end: Option<&'a str>,
periods: Option<usize>,
pandas_freq: &'a str,
name: Option<&'a str>,
}
#[derive(Debug, Clone, Copy)]
struct DateOffsetCase<'a> {
case_id: &'a str,
timestamp: &'a str,
pandas_offset: &'a str,
offset: DateOffset,
}
#[derive(Debug, Clone, Copy)]
struct InferFreqCase<'a> {
case_id: &'a str,
timestamps: &'a [&'a str],
}
#[derive(Debug, Deserialize)]
struct PandasDateRange {
values: Vec<i64>,
name: Option<String>,
}
#[derive(Debug, Deserialize)]
struct PandasOffsetResult {
value: i64,
}
#[derive(Debug, Deserialize)]
struct PandasInferFreqResult {
ok: bool,
value: Option<String>,
error_type: Option<String>,
message: Option<String>,
}
fn pandas_tseries_range_or_skip(
config: &HarnessConfig,
case: PandasRangeOracleCase<'_>,
) -> Result<Option<PandasDateRange>, String> {
let case_id = case.case_id;
let pandas_function = case.pandas_function;
if !config.oracle_root.exists() && !config.allows_live_oracle_fallback() {
eprintln!(
"live pandas unavailable; skipping tseries conformance test {}: legacy oracle root does not exist: {}",
case_id,
config.oracle_root.display()
);
return Ok(None);
}
let payload = serde_json::json!({
"function": pandas_function,
"start": case.start,
"end": case.end,
"periods": case.periods,
"freq": case.pandas_freq,
"name": case.name,
});
let script = r#"
import importlib
import json
import os
import sys
legacy_root = os.path.abspath(sys.argv[1])
allow_system_fallback = sys.argv[2] == "1"
candidate_parent = os.path.dirname(legacy_root)
if os.path.isdir(candidate_parent):
sys.path.insert(0, candidate_parent)
try:
import pandas as pd
except Exception:
if not allow_system_fallback:
raise
while candidate_parent in sys.path:
sys.path.remove(candidate_parent)
sys.modules.pop("pandas", None)
pd = importlib.import_module("pandas")
request = json.loads(sys.stdin.read())
kwargs = {"freq": request["freq"]}
for key in ("start", "end", "periods", "name"):
if request.get(key) is not None:
kwargs[key] = request[key]
index = getattr(pd, request["function"])(**kwargs)
if hasattr(index, "as_unit"):
index = index.as_unit("ns")
print(json.dumps({
"values": [int(value) for value in index.asi8.tolist()],
"name": index.name,
}, separators=(",", ":")))
"#;
let mut child = Command::new(&config.python_bin)
.arg("-c")
.arg(script)
.arg(&config.oracle_root)
.arg(if config.allows_live_oracle_fallback() {
"1"
} else {
"0"
})
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|err| format!("spawn pandas {pandas_function} oracle failed: {err}"))?;
let stdin_handle = child
.stdin
.take()
.ok_or_else(|| format!("pandas {pandas_function} oracle stdin unavailable"))?;
let payload_bytes = payload.to_string().into_bytes();
let stdin_writer = std::thread::spawn(move || -> std::io::Result<()> {
let mut stdin = stdin_handle;
stdin.write_all(&payload_bytes)
});
let output = child
.wait_with_output()
.map_err(|err| format!("wait for pandas {pandas_function} oracle failed: {err}"))?;
let pandas_function_for_err = pandas_function.to_string();
stdin_writer
.join()
.map_err(|_| format!("pandas {pandas_function_for_err} oracle stdin writer panicked"))?
.map_err(|err| {
format!("write pandas {pandas_function_for_err} oracle payload failed: {err}")
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("AttributeError: module 'pandas' has no attribute") {
eprintln!(
"live pandas unavailable; skipping tseries conformance test {case_id}: {stderr}"
);
return Ok(None);
}
return Err(format!(
"pandas {pandas_function} oracle failed for {case_id}: {}",
stderr
));
}
serde_json::from_slice(&output.stdout)
.map(Some)
.map_err(|err| format!("parse pandas {pandas_function} json failed: {err}"))
}
fn pandas_date_range_or_skip(
config: &HarnessConfig,
case: DateRangeCase<'_>,
) -> Result<Option<PandasDateRange>, String> {
pandas_tseries_range_or_skip(
config,
PandasRangeOracleCase {
case_id: case.case_id,
pandas_function: "date_range",
start: case.start,
end: case.end,
periods: case.periods,
pandas_freq: case.pandas_freq,
name: case.name,
},
)
}
fn pandas_bdate_range_or_skip(
config: &HarnessConfig,
case: BusinessDateRangeCase<'_>,
) -> Result<Option<PandasDateRange>, String> {
pandas_tseries_range_or_skip(
config,
PandasRangeOracleCase {
case_id: case.case_id,
pandas_function: "bdate_range",
start: case.start,
end: case.end,
periods: case.periods,
pandas_freq: "B",
name: case.name,
},
)
}
fn pandas_offset_or_skip(
config: &HarnessConfig,
case: DateOffsetCase<'_>,
) -> Result<Option<PandasOffsetResult>, String> {
if !config.oracle_root.exists() && !config.allows_live_oracle_fallback() {
eprintln!(
"live pandas unavailable; skipping tseries conformance test {}: legacy oracle root does not exist: {}",
case.case_id,
config.oracle_root.display()
);
return Ok(None);
}
let n = match case.offset {
DateOffset::Day(value) | DateOffset::BusinessDay(value) | DateOffset::MonthEnd(value) => {
value
}
};
let payload = serde_json::json!({
"timestamp": case.timestamp,
"offset": case.pandas_offset,
"n": n,
});
let script = r#"
import importlib
import json
import os
import sys
legacy_root = os.path.abspath(sys.argv[1])
allow_system_fallback = sys.argv[2] == "1"
candidate_parent = os.path.dirname(legacy_root)
if os.path.isdir(candidate_parent):
sys.path.insert(0, candidate_parent)
try:
import pandas as pd
except Exception:
if not allow_system_fallback:
raise
while candidate_parent in sys.path:
sys.path.remove(candidate_parent)
sys.modules.pop("pandas", None)
pd = importlib.import_module("pandas")
request = json.loads(sys.stdin.read())
offsets = {
"Day": pd.offsets.Day,
"BDay": pd.offsets.BDay,
"MonthEnd": pd.offsets.MonthEnd,
}
result = pd.Timestamp(request["timestamp"]) + offsets[request["offset"]](request["n"])
print(json.dumps({"value": int(result.value)}, separators=(",", ":")))
"#;
let mut child = Command::new(&config.python_bin)
.arg("-c")
.arg(script)
.arg(&config.oracle_root)
.arg(if config.allows_live_oracle_fallback() {
"1"
} else {
"0"
})
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|err| format!("spawn pandas offset oracle failed: {err}"))?;
let stdin_handle = child
.stdin
.take()
.ok_or_else(|| "pandas offset oracle stdin unavailable".to_owned())?;
let payload_bytes = payload.to_string().into_bytes();
let stdin_writer = std::thread::spawn(move || -> std::io::Result<()> {
let mut stdin = stdin_handle;
stdin.write_all(&payload_bytes)
});
let output = child
.wait_with_output()
.map_err(|err| format!("wait for pandas offset oracle failed: {err}"))?;
stdin_writer
.join()
.map_err(|_| "pandas offset oracle stdin writer panicked".to_owned())?
.map_err(|err| format!("write pandas offset oracle payload failed: {err}"))?;
if !output.status.success() {
return Err(format!(
"pandas offset oracle failed for {}: {}",
case.case_id,
String::from_utf8_lossy(&output.stderr)
));
}
serde_json::from_slice(&output.stdout)
.map(Some)
.map_err(|err| format!("parse pandas offset json failed: {err}"))
}
fn pandas_infer_freq_or_skip(
config: &HarnessConfig,
case: InferFreqCase<'_>,
) -> Result<Option<PandasInferFreqResult>, String> {
if !config.oracle_root.exists() && !config.allows_live_oracle_fallback() {
eprintln!(
"live pandas unavailable; skipping tseries conformance test {}: legacy oracle root does not exist: {}",
case.case_id,
config.oracle_root.display()
);
return Ok(None);
}
let payload = serde_json::json!({
"timestamps": case.timestamps,
});
let script = r#"
import importlib
import json
import os
import sys
legacy_root = os.path.abspath(sys.argv[1])
allow_system_fallback = sys.argv[2] == "1"
candidate_parent = os.path.dirname(legacy_root)
if os.path.isdir(candidate_parent):
sys.path.insert(0, candidate_parent)
try:
import pandas as pd
except Exception:
if not allow_system_fallback:
raise
while candidate_parent in sys.path:
sys.path.remove(candidate_parent)
sys.modules.pop("pandas", None)
pd = importlib.import_module("pandas")
request = json.loads(sys.stdin.read())
try:
value = pd.infer_freq(pd.DatetimeIndex(request["timestamps"]))
print(json.dumps({"ok": True, "value": value}, separators=(",", ":")))
except Exception as exc:
print(json.dumps({
"ok": False,
"error_type": type(exc).__name__,
"message": str(exc),
}, separators=(",", ":")))
"#;
let mut child = Command::new(&config.python_bin)
.arg("-c")
.arg(script)
.arg(&config.oracle_root)
.arg(if config.allows_live_oracle_fallback() {
"1"
} else {
"0"
})
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|err| format!("spawn pandas infer_freq oracle failed: {err}"))?;
let stdin_handle = child
.stdin
.take()
.ok_or_else(|| "pandas infer_freq oracle stdin unavailable".to_owned())?;
let payload_bytes = payload.to_string().into_bytes();
let stdin_writer = std::thread::spawn(move || -> std::io::Result<()> {
let mut stdin = stdin_handle;
stdin.write_all(&payload_bytes)
});
let output = child
.wait_with_output()
.map_err(|err| format!("wait for pandas infer_freq oracle failed: {err}"))?;
stdin_writer
.join()
.map_err(|_| "pandas infer_freq oracle stdin writer panicked".to_owned())?
.map_err(|err| format!("write pandas infer_freq oracle payload failed: {err}"))?;
if !output.status.success() {
return Err(format!(
"pandas infer_freq oracle failed for {}: {}",
case.case_id,
String::from_utf8_lossy(&output.stderr)
));
}
serde_json::from_slice(&output.stdout)
.map(Some)
.map_err(|err| format!("parse pandas infer_freq json failed: {err}"))
}
fn datetime_labels(index: &Index) -> Result<Vec<i64>, String> {
index
.labels()
.iter()
.map(|label| match label {
IndexLabel::Datetime64(value) => Ok(*value),
other => Err(format!("expected datetime64 label, got {other:?}")),
})
.collect()
}
fn assert_date_range_matches_pandas(case: DateRangeCase<'_>) -> Result<(), String> {
let mut config = HarnessConfig::default_paths();
config.allow_system_pandas_fallback = true;
let Some(expected) = pandas_date_range_or_skip(&config, case)? else {
return Ok(());
};
let actual = date_range(
case.start,
case.end,
case.periods,
case.rust_freq_nanos,
case.name,
)
.map_err(|err| format!("fp date_range failed for {}: {err}", case.case_id))?;
assert_eq!(
datetime_labels(&actual)?,
expected.values,
"pandas.tseries date_range value parity drift for {}",
case.case_id
);
assert_eq!(
actual.name().map(String::from),
expected.name,
"pandas.tseries date_range name parity drift for {}",
case.case_id
);
Ok(())
}
fn assert_date_range_rejects_like_pandas<F>(
case: DateRangeCase<'_>,
expected_pandas_error: &str,
matches_fp_error: F,
) -> Result<(), String>
where
F: FnOnce(&DateRangeError) -> bool,
{
let mut config = HarnessConfig::default_paths();
config.allow_system_pandas_fallback = true;
match pandas_date_range_or_skip(&config, case) {
Ok(None) => return Ok(()),
Ok(Some(expected)) => {
return Err(format!(
"pandas date_range unexpectedly accepted {} with values {:?}",
case.case_id, expected.values
));
}
Err(err) if err.contains(expected_pandas_error) => {}
Err(err) => {
return Err(format!(
"pandas date_range rejected {} with unexpected error: {err}",
case.case_id
));
}
}
let actual = date_range(
case.start,
case.end,
case.periods,
case.rust_freq_nanos,
case.name,
);
let actual_err = match actual {
Ok(index) => {
return Err(format!(
"fp date_range unexpectedly accepted {} with labels {:?}",
case.case_id,
index.labels()
));
}
Err(err) => err,
};
assert!(
matches_fp_error(&actual_err),
"pandas.tseries date_range error parity drift for {}: fp returned {actual_err:?}",
case.case_id
);
Ok(())
}
fn assert_bdate_range_matches_pandas(case: BusinessDateRangeCase<'_>) -> Result<(), String> {
let mut config = HarnessConfig::default_paths();
config.allow_system_pandas_fallback = true;
let Some(expected) = pandas_bdate_range_or_skip(&config, case)? else {
return Ok(());
};
let actual = bdate_range(case.start, case.end, case.periods, case.name)
.map_err(|err| format!("fp bdate_range failed for {}: {err}", case.case_id))?;
assert_eq!(
datetime_labels(&actual)?,
expected.values,
"pandas.tseries bdate_range value parity drift for {}",
case.case_id
);
assert_eq!(
actual.name().map(String::from),
expected.name,
"pandas.tseries bdate_range name parity drift for {}",
case.case_id
);
Ok(())
}
fn assert_offset_matches_pandas(case: DateOffsetCase<'_>) -> Result<(), String> {
let mut config = HarnessConfig::default_paths();
config.allow_system_pandas_fallback = true;
let Some(expected) = pandas_offset_or_skip(&config, case)? else {
return Ok(());
};
let actual = apply_date_offset(case.timestamp, case.offset)
.map_err(|err| format!("fp offset failed for {}: {err}", case.case_id))?;
assert_eq!(
actual, expected.value,
"pandas.tseries offset value parity drift for {}",
case.case_id
);
Ok(())
}
fn assert_infer_freq_matches_pandas(case: InferFreqCase<'_>) -> Result<(), String> {
let mut config = HarnessConfig::default_paths();
config.allow_system_pandas_fallback = true;
let Some(expected) = pandas_infer_freq_or_skip(&config, case)? else {
return Ok(());
};
let actual = infer_freq_from_timestamps(case.timestamps);
if expected.ok {
let actual =
actual.map_err(|err| format!("fp infer_freq failed for {}: {err}", case.case_id))?;
assert_eq!(
actual, expected.value,
"pandas.tseries infer_freq value parity drift for {}",
case.case_id
);
} else {
assert!(
actual.is_err(),
"pandas.tseries infer_freq error parity drift for {}: pandas returned {:?} {:?}, fp returned {:?}",
case.case_id,
expected.error_type,
expected.message,
actual
);
}
Ok(())
}
#[test]
fn conformance_tseries_date_range_start_periods_daily() -> Result<(), String> {
assert_date_range_matches_pandas(DateRangeCase {
case_id: "tseries_date_range_start_periods_daily",
start: Some("2024-01-01"),
end: None,
periods: Some(3),
pandas_freq: "D",
rust_freq_nanos: Timedelta::NANOS_PER_DAY,
name: None,
})
}
#[test]
fn conformance_tseries_date_range_start_end_two_day_frequency() -> Result<(), String> {
assert_date_range_matches_pandas(DateRangeCase {
case_id: "tseries_date_range_start_end_two_day_frequency",
start: Some("2024-01-01"),
end: Some("2024-01-07"),
periods: None,
pandas_freq: "2D",
rust_freq_nanos: 2 * Timedelta::NANOS_PER_DAY,
name: None,
})
}
#[test]
fn conformance_tseries_date_range_end_periods_hourly() -> Result<(), String> {
assert_date_range_matches_pandas(DateRangeCase {
case_id: "tseries_date_range_end_periods_hourly",
start: None,
end: Some("2024-01-01 12:00:00"),
periods: Some(3),
pandas_freq: "6h",
rust_freq_nanos: 6 * Timedelta::NANOS_PER_HOUR,
name: None,
})
}
#[test]
fn conformance_tseries_date_range_zero_periods() -> Result<(), String> {
assert_date_range_matches_pandas(DateRangeCase {
case_id: "tseries_date_range_zero_periods",
start: Some("2024-01-01"),
end: None,
periods: Some(0),
pandas_freq: "D",
rust_freq_nanos: Timedelta::NANOS_PER_DAY,
name: None,
})
}
#[test]
fn conformance_tseries_date_range_preserves_name() -> Result<(), String> {
assert_date_range_matches_pandas(DateRangeCase {
case_id: "tseries_date_range_preserves_name",
start: Some("2024-02-01"),
end: None,
periods: Some(2),
pandas_freq: "D",
rust_freq_nanos: Timedelta::NANOS_PER_DAY,
name: Some("when"),
})
}
#[test]
fn conformance_tseries_date_range_rejects_over_specified_params() -> Result<(), String> {
assert_date_range_rejects_like_pandas(
DateRangeCase {
case_id: "tseries_date_range_rejects_over_specified_params",
start: Some("2020-01-01"),
end: Some("2020-01-03"),
periods: Some(2),
pandas_freq: "D",
rust_freq_nanos: Timedelta::NANOS_PER_DAY,
name: None,
},
"ValueError",
|err| matches!(err, DateRangeError::TooManyParams),
)
}
#[test]
fn conformance_tseries_bdate_range_start_periods_weekdays() -> Result<(), String> {
assert_bdate_range_matches_pandas(BusinessDateRangeCase {
case_id: "tseries_bdate_range_start_periods_weekdays",
start: Some("2024-01-01"),
end: None,
periods: Some(3),
name: None,
})
}
#[test]
fn conformance_tseries_bdate_range_weekend_start_rolls_forward() -> Result<(), String> {
assert_bdate_range_matches_pandas(BusinessDateRangeCase {
case_id: "tseries_bdate_range_weekend_start_rolls_forward",
start: Some("2024-01-06"),
end: None,
periods: Some(3),
name: None,
})
}
#[test]
fn conformance_tseries_bdate_range_start_end_skips_weekend() -> Result<(), String> {
assert_bdate_range_matches_pandas(BusinessDateRangeCase {
case_id: "tseries_bdate_range_start_end_skips_weekend",
start: Some("2024-01-05"),
end: Some("2024-01-09"),
periods: None,
name: None,
})
}
#[test]
fn conformance_tseries_bdate_range_weekend_end_rolls_backward() -> Result<(), String> {
assert_bdate_range_matches_pandas(BusinessDateRangeCase {
case_id: "tseries_bdate_range_weekend_end_rolls_backward",
start: None,
end: Some("2024-01-07"),
periods: Some(3),
name: None,
})
}
#[test]
fn conformance_tseries_bdate_range_zero_periods() -> Result<(), String> {
assert_bdate_range_matches_pandas(BusinessDateRangeCase {
case_id: "tseries_bdate_range_zero_periods",
start: Some("2024-01-01"),
end: None,
periods: Some(0),
name: None,
})
}
#[test]
fn conformance_tseries_bdate_range_preserves_name() -> Result<(), String> {
assert_bdate_range_matches_pandas(BusinessDateRangeCase {
case_id: "tseries_bdate_range_preserves_name",
start: Some("2024-02-02"),
end: None,
periods: Some(2),
name: Some("bizday"),
})
}
#[test]
fn conformance_tseries_offsets_day_preserves_time() -> Result<(), String> {
assert_offset_matches_pandas(DateOffsetCase {
case_id: "tseries_offsets_day_preserves_time",
timestamp: "2024-01-01 12:30:00",
pandas_offset: "Day",
offset: DateOffset::Day(2),
})
}
#[test]
fn conformance_tseries_offsets_day_zero_is_noop() -> Result<(), String> {
assert_offset_matches_pandas(DateOffsetCase {
case_id: "tseries_offsets_day_zero_is_noop",
timestamp: "2024-01-01",
pandas_offset: "Day",
offset: DateOffset::Day(0),
})
}
#[test]
fn conformance_tseries_offsets_bday_friday_to_monday() -> Result<(), String> {
assert_offset_matches_pandas(DateOffsetCase {
case_id: "tseries_offsets_bday_friday_to_monday",
timestamp: "2024-01-05",
pandas_offset: "BDay",
offset: DateOffset::BusinessDay(1),
})
}
#[test]
fn conformance_tseries_offsets_bday_weekend_rolls_forward() -> Result<(), String> {
assert_offset_matches_pandas(DateOffsetCase {
case_id: "tseries_offsets_bday_weekend_rolls_forward",
timestamp: "2024-01-06",
pandas_offset: "BDay",
offset: DateOffset::BusinessDay(1),
})
}
#[test]
fn conformance_tseries_offsets_bday_negative_skips_weekend() -> Result<(), String> {
assert_offset_matches_pandas(DateOffsetCase {
case_id: "tseries_offsets_bday_negative_skips_weekend",
timestamp: "2024-01-08",
pandas_offset: "BDay",
offset: DateOffset::BusinessDay(-1),
})
}
#[test]
fn conformance_tseries_offsets_bday_zero_weekend_rolls_forward() -> Result<(), String> {
assert_offset_matches_pandas(DateOffsetCase {
case_id: "tseries_offsets_bday_zero_weekend_rolls_forward",
timestamp: "2024-01-06",
pandas_offset: "BDay",
offset: DateOffset::BusinessDay(0),
})
}
#[test]
fn conformance_tseries_offsets_month_end_midmonth_leap_year() -> Result<(), String> {
assert_offset_matches_pandas(DateOffsetCase {
case_id: "tseries_offsets_month_end_midmonth_leap_year",
timestamp: "2024-02-10",
pandas_offset: "MonthEnd",
offset: DateOffset::MonthEnd(1),
})
}
#[test]
fn conformance_tseries_offsets_month_end_on_anchor_advances() -> Result<(), String> {
assert_offset_matches_pandas(DateOffsetCase {
case_id: "tseries_offsets_month_end_on_anchor_advances",
timestamp: "2024-02-29",
pandas_offset: "MonthEnd",
offset: DateOffset::MonthEnd(1),
})
}
#[test]
fn conformance_tseries_offsets_month_end_negative_rolls_back() -> Result<(), String> {
assert_offset_matches_pandas(DateOffsetCase {
case_id: "tseries_offsets_month_end_negative_rolls_back",
timestamp: "2024-02-10",
pandas_offset: "MonthEnd",
offset: DateOffset::MonthEnd(-1),
})
}
#[test]
fn conformance_tseries_infer_freq_daily() -> Result<(), String> {
assert_infer_freq_matches_pandas(InferFreqCase {
case_id: "tseries_infer_freq_daily",
timestamps: &["2024-01-01", "2024-01-02", "2024-01-03", "2024-01-04"],
})
}
#[test]
fn conformance_tseries_infer_freq_two_day() -> Result<(), String> {
assert_infer_freq_matches_pandas(InferFreqCase {
case_id: "tseries_infer_freq_two_day",
timestamps: &["2024-01-01", "2024-01-03", "2024-01-05", "2024-01-07"],
})
}
#[test]
fn conformance_tseries_infer_freq_six_hour() -> Result<(), String> {
assert_infer_freq_matches_pandas(InferFreqCase {
case_id: "tseries_infer_freq_six_hour",
timestamps: &[
"2024-01-01 00:00:00",
"2024-01-01 06:00:00",
"2024-01-01 12:00:00",
"2024-01-01 18:00:00",
],
})
}
#[test]
fn conformance_tseries_infer_freq_business_day() -> Result<(), String> {
assert_infer_freq_matches_pandas(InferFreqCase {
case_id: "tseries_infer_freq_business_day",
timestamps: &[
"2024-01-01",
"2024-01-02",
"2024-01-03",
"2024-01-04",
"2024-01-05",
"2024-01-08",
"2024-01-09",
],
})
}
#[test]
fn conformance_tseries_infer_freq_month_end_leap_year() -> Result<(), String> {
assert_infer_freq_matches_pandas(InferFreqCase {
case_id: "tseries_infer_freq_month_end_leap_year",
timestamps: &["2024-01-31", "2024-02-29", "2024-03-31", "2024-04-30"],
})
}
#[test]
fn conformance_tseries_infer_freq_irregular_returns_none() -> Result<(), String> {
assert_infer_freq_matches_pandas(InferFreqCase {
case_id: "tseries_infer_freq_irregular_returns_none",
timestamps: &["2024-01-01", "2024-01-02", "2024-01-04", "2024-01-07"],
})
}
#[test]
fn conformance_tseries_infer_freq_duplicate_returns_none() -> Result<(), String> {
assert_infer_freq_matches_pandas(InferFreqCase {
case_id: "tseries_infer_freq_duplicate_returns_none",
timestamps: &["2024-01-01", "2024-01-02", "2024-01-02", "2024-01-03"],
})
}
#[test]
fn conformance_tseries_infer_freq_too_few_dates_errors() -> Result<(), String> {
assert_infer_freq_matches_pandas(InferFreqCase {
case_id: "tseries_infer_freq_too_few_dates_errors",
timestamps: &["2024-01-01", "2024-01-02"],
})
}