cftime-rs 0.1.4

Rust implementation of cftime
Documentation
# `cftime-rs`

`cftime-rs` is an implementation in `rust` of the [cf time](https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/cf-conventions.html#time-coordinate) convention. Python bindins are available for this project. 

<p align="center">
    <a href="https://github.com/antscloud/cftime-rs/actions">
        <img alt="CI Status" src="https://github.com/antscloud/cftime-rs/actions/workflows/ci.yaml/badge.svg?branch=main">
    </a>
    <a href="https://crates.io/crates/cftime-rs">
        <img src="https://img.shields.io/crates/v/cftime-rs.svg" alt="Crates.io version badge">
    </a>
    <a href="https://pypi.org/project/cftime-rs/">
        <img src="https://img.shields.io/pypi/v/cftime-rs.svg" alt="Pypi version badge">
    </a>
    <a href="https://github.com/antscloud/cftime-rs/actions">
        <img src="https://github.com/antscloud/cftime-rs/actions/workflows/ci.yaml/badge.svg" alt="CI/CD Status">
    </a>
    <a href="https://www.gnu.org/licenses/agpl-3.0">
        <img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg" alt="License: AGPL v3">
    </a>
    <a href="https://cftime-rs.readthedocs.io/en/latest">
        <img alt="Documentation Status" src="https://readthedocs.org/projects/cftime-rs/badge/?version=latest">
    </a>
    <a href="https://github.com/antscloud/cftime-rs/issues">
        <img alt="Issue Badge" src="https://img.shields.io/github/issues/antscloud/cftime-rs">
    </a>
    <a href="https://github.com/antscloud/cftime-rs/pulls">
        <img alt="Pull requests Badge" src="https://img.shields.io/github/issues-pr/antscloud/cftime-rs">
    </a>
</p>

<p align="center">
    <b>Documentation</b> :
    <a href="https://docs.rs/cftime-rs/latest/cftime_rs/">Rust</a>
    |
    <a href="https://cftime-rs.readthedocs.io/en/latest/">Python</a>
    <br>
    <b>Packages</b> :
    <a href="https://crates.io/crates/cftime-rs">Rust</a>
    |
    <a href="https://pypi.org/project/cftime-rs/">Python</a>
    <br>
    <b>Repository</b> :
    <a href="https://github.com/antscloud/cftime-rs">Github</a>
</p>



## Rust
### Installation

```
cargo install cftime-rs
```

### Examples 

#### Decoding 

Decoding needs units, and calendar and can work with `i32`, `i64`, `f32`, ``f64`` and their corresponding vector type `Vec<i32>`, `Vec<i64>`, `Vec<f32>` and `Vec<f64>`. From these type it return either a `CFDatetime` object or a `Vec<CFDatetime>`.

```rust
use cftime_rs::calendars::Calendar;
use cftime_rs::decoder::*;
use std::str::FromStr;
fn main() {
    let to_decode = vec![0, 1, 2, 3, 4, 5];
    let units = "days since 2000-01-01 00:00:00";
    let calendar = Calendar::from_str("standard").unwrap();
    let datetimes = to_decode.decode_cf(units, calendar).unwrap();
    for datetime in datetimes {
        println!("{}", datetime);
    }
}
```

will print :

```
2000-01-01 00:00:00.000
2000-01-02 00:00:00.000
2000-01-03 00:00:00.000
2000-01-04 00:00:00.000
2000-01-05 00:00:00.000
2000-01-06 00:00:00.000
```

#### Encoding 

Encoding needs units and calendar and can convert a `CFDatetime` object into an `i32`, `i64`, `f32` or  `f64` or a `Vec<CFDatetime>` into `Vec<i32>`, `Vec<i64>`, `Vec<f32>` or `Vec<f64>`.

```rust
use cftime_rs::calendars::Calendar;
use cftime_rs::datetime::CFDatetime;
use cftime_rs::encoder::*;
use cftime_rs::errors::Error;
use std::str::FromStr;
fn main() {
    let calendar = Calendar::from_str("standard").unwrap();
    // Create vector of datetimes and convert Vec<Result<CFDatetime, Error>>
    // into Result<Vec<CFDatetime>, Error>
    let to_encode: Result<Vec<CFDatetime>, Error> = vec![
        CFDatetime::from_ymd(2000, 1, 1, calendar),
        CFDatetime::from_ymd(2000, 1, 2, calendar),
        CFDatetime::from_ymd(2000, 1, 3, calendar),
        CFDatetime::from_ymd(2000, 1, 4, calendar),
        CFDatetime::from_ymd(2000, 1, 5, calendar),
        CFDatetime::from_ymd(2000, 1, 6, calendar),
    ]
    .into_iter()
    .collect();
    // define the units
    let units = "days since 2000-01-01 00:00:00";
    // The type annotation for result allow us to cast to type we want
    // here we use Vec<i64>
    let results: Vec<i64> = to_encode.unwrap().encode_cf(units, calendar).unwrap();
    for result in results {
        println!("{}", result);
    }
}
```

will print :

```
0
1
2
3
4
5
```

## Python
### Installation

```
pip install cftime-rs
```

### Examples 


### Decoding to PyCfDatetimes

```python
import cftime_rs

to_decode = [0, 1, 2, 3, 4, 5]
units = "days since 2000-01-01 00:00:00"
calendar = "standard"
datetimes = cftime_rs.num2date(arr, units, calendar)
for datetime in datetimes:
    print(datetime)
```

will print :

```
2000-01-01 00:00:00.000
2000-01-02 00:00:00.000
2000-01-03 00:00:00.000
2000-01-04 00:00:00.000
2000-01-05 00:00:00.000
2000-01-06 00:00:00.000
```

### Encoding PyCFDatetimes

```python
calendar = cftime_rs.PyCFCalendar.from_str("standard")
to_encode = [
    cftime_rs.PyCFDatetime.from_ymd(2000, 1, 1, calendar),
    cftime_rs.PyCFDatetime.from_ymd(2000, 1, 2, calendar),
    cftime_rs.PyCFDatetime.from_ymd(2000, 1, 3, calendar),
    cftime_rs.PyCFDatetime.from_ymd(2000, 1, 4, calendar),
    cftime_rs.PyCFDatetime.from_ymd(2000, 1, 5, calendar),
    cftime_rs.PyCFDatetime.from_ymd(2000, 1, 6, calendar),
]
units = "days since 2000-01-01 00:00:00"
calendar = "standard"
numbers = cftime_rs.date2num(to_encode, units, calendar, dtype="int")
for number in numbers:
    print(number)
```

will print :

```
0
1
2
3
4
5
```

### Decoding to Python datetimes

```python
to_decode = [0, 1, 2, 3, 4, 5]
units = "days since 2000-01-01 00:00:00"
calendar = "standard"
datetimes = cftime_rs.num2pydate(to_decode, units, calendar)
for datetime in datetimes:
    print(datetime)
```
will print 

```
2000-01-01 01:00:00
2000-01-02 01:00:00
2000-01-03 01:00:00
2000-01-04 01:00:00
2000-01-05 01:00:00
2000-01-06 01:00:00
```

### Decoding Python datetimes

```python
to_encode = [
    dt.datetime(2000, 1, 1),
    dt.datetime(2000, 1, 2),
    dt.datetime(2000, 1, 3),
    dt.datetime(2000, 1, 4),
    dt.datetime(2000, 1, 5),
    dt.datetime(2000, 1, 6),
]
units = "days since 2000-01-01 00:00:00"
calendar = "standard"
numbers = cftime_rs.pydate2num(to_encode, units, calendar, dtype="int")
for number in numbers:
    print(number)
```

will print 

```
0
1
2
3
4
5
```

## Known issues
While this date calculation library can handle a wide range of dates, from approximately -291,672,107,014 BC to 291,672,107,014 AD, there are some performance considerations you should be aware of.
As you move further away from the reference date of 1970-01-01 00:00:00, the time of calculation increases. This is because the library needs to account for leap years in various calendars.

Here is an example of the computation of 1_000_000_000_000_000 seconds using the units "seconds since 2000-01-01 00:00:00" on my personal computer in release mode :

| Calendar          | Computation Time |
|-------------------|------------------|
| Standard Calendar | 44.470405ms      |
| Leap Day Calendar | 8.052179ms       |
| 360-Day Calendar  | 12.834µs         |

## Comparison with cftime

Here is a benchmark on my computer of two methods. This is not really rigorous but this is to give an idea.  

We are comparing cftime with cftime_rs. The first method involves decoding a series of numbers using the standard calendar, calling the .str() method, and then re-encoding them to the same unit and calendar. The second method is to decode a series of numbers using the standard calendar and re-encode them to the same unit and calendar without calling .str(). Both methods are designed to be fair between the two libraries because cftime_rs only uses timestamps (i64) as its internal representation and never recalculates the year, month, day, hour, minutes, and seconds, except if you explicitly request this representation.


<div style="display: flex; align-items: center; justify-content: center">
    <img src="./benchmark/performance_comparison_with_str.png" alt="First method" width="45%">
    <img src="./benchmark/performance_comparison_without_str.png" alt="Second method" width="45%">
</div>