Skip to content

Commit

Permalink
Merge pull request #12 from xoen/removed-duplication
Browse files Browse the repository at this point in the history
Simplified public interface and removed duplication
  • Loading branch information
jnioche authored Oct 3, 2024
2 parents 6217015 + 2f3c5c7 commit 031fc5e
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 134 deletions.
9 changes: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,18 +82,15 @@ You can use the library in your Rust project by adding it to cargo with
then declaring it in your code

```Rust
use carbonintensity::{
get_intensities_postcode, get_intensities_region, get_intensity_postcode, get_intensity_region,
ApiError,
};
use carbonintensity::{get_intensity, Target, Region};

...

let result = get_intensity_postcode(postcode).await;
let scotland = Region::Scotland;
let result = get_intensity(&Target::Region(scotland)).await;

```

## License

This project is provided under [Apache License](http://www.apache.org/licenses/LICENSE-2.0).

129 changes: 55 additions & 74 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ use thiserror::Error;
use url::ParseError;

mod region;
mod target;

pub use region::Region;
pub use target::Target;

/// An error communicating with the Carbon Intensity API.
#[derive(Debug, Error)]
Expand Down Expand Up @@ -67,30 +69,29 @@ struct PowerData {
data: RegionData,
}

static BASE_URL: &str = "https://api.carbonintensity.org.uk/";
static BASE_URL: &str = "https://api.carbonintensity.org.uk";

/// Current carbon intensity for a postcode
/// Current carbon intensity for a target (e.g. a region)
///
/// <https://api.carbonintensity.org.uk/regional/postcode/>
///
pub async fn get_intensity_postcode(postcode: &str) -> Result<i32, ApiError> {
if postcode.len() < 2 || postcode.len() > 4 {
return Err(ApiError::Error("Invalid postcode".to_string()));
}

let path = "regional/postcode/";
let url = format!("{BASE_URL}{path}{postcode}");
get_intensity(&url).await
}
/// Uses either
/// - <https://api.carbonintensity.org.uk/regional/postcode/>
/// - <https://api.carbonintensity.org.uk/regional/regionid/>
pub async fn get_intensity(target: &Target) -> Result<i32, ApiError> {
let path = match target {
Target::Postcode(postcode) => {
if postcode.len() < 2 || postcode.len() > 4 {
return Err(ApiError::Error("Invalid postcode".to_string()));
}
format!("regional/postcode/{postcode}")
}
&Target::Region(region) => {
let region_id = region as u8;
format!("regional/regionid/{region_id}")
}
};

/// Current carbon intensity for a region
///
/// <https://api.carbonintensity.org.uk/regional/regionid/>
pub async fn get_intensity_region(region: Region) -> Result<i32, ApiError> {
let path = "regional/regionid/";
let region_id = region as u8;
let url = format!("{BASE_URL}{path}{region_id}");
get_intensity(&url).await
let url = format!("{BASE_URL}/{path}");
get_intensity_for_url(&url).await
}

fn parse(date: &str) -> Result<NaiveDateTime, chrono::ParseError> {
Expand Down Expand Up @@ -161,68 +162,48 @@ fn normalise_dates(
Ok(ranges)
}

/// Return a vector containing the intensity measures
/// per 30 min window for a given region
pub async fn get_intensities_region(
region: Region,
start: &str,
end: &Option<&str>,
) -> Result<Vec<(NaiveDateTime, i32)>, ApiError> {
let path = "regional/intensity/";
let region_id = region as u8;

let ranges = normalise_dates(start, end)?;

let mut output = Vec::new();

// TODO query in parallel
for r in ranges {
// shift dates by one minute
let start_date = r.0 + Duration::minutes(1);
let end_date = r.1 + Duration::minutes(1);

let url = format!(
"{BASE_URL}{path}{}/{}/regionid/{region_id}",
start_date.format("%Y-%m-%dT%H:%MZ"),
end_date.format("%Y-%m-%dT%H:%MZ"),
);
let region_data = get_intensities(&url).await?;
let mut tuples = to_tuple(region_data)?;
output.append(&mut tuples);
}
Ok(output)
}

/// ISO8601 format YYYY-MM-DDThh:mmZ
/// but tolerates YYYY-MM-DD
/// https://api.carbonintensity.org.uk/regional/intensity/2023-05-15/2023-05-20/postcode/RG10
/// Get intensities for a given target (region or postcode) in 30 minutes windows
///
/// Dates are strings in ISO-8601 format YYYY-MM-DDThh:mmZ
/// but YYYY-MM-DD is tolerated
///
pub async fn get_intensities_postcode(
postcode: &str,
/// Uses either
/// - https://api.carbonintensity.org.uk/regional/intensity/2023-05-15/2023-05-20/postcode/RG10
/// - https://api.carbonintensity.org.uk/regional/intensity/2023-05-15/2023-05-20/regionid/13
pub async fn get_intensities(
target: &Target,
start: &str,
end: &Option<&str>,
) -> Result<Vec<(NaiveDateTime, i32)>, ApiError> {
if postcode.len() < 2 || postcode.len() > 4 {
return Err(ApiError::Error("Invalid postcode".to_string()));
}
let path = match target {
Target::Postcode(postcode) => {
if postcode.len() < 2 || postcode.len() > 4 {
return Err(ApiError::Error("Invalid postcode".to_string()));
}

format!("postcode/{postcode}")
}
&Target::Region(region) => {
let region_id = region as u8;
format!("regionid/{region_id}")
}
};

let ranges = normalise_dates(start, end)?;

let mut output = Vec::new();
let path = "regional/intensity/";

// TODO query in parallel
for r in ranges {
for (start_date, end_date) in ranges {
// shift dates by one minute
let start_date = r.0 + Duration::minutes(1);
let end_date = r.1 + Duration::minutes(1);

let url = format!(
"{BASE_URL}{path}{}/{}/postcode/{postcode}",
start_date.format("%Y-%m-%dT%H:%MZ"),
end_date.format("%Y-%m-%dT%H:%MZ"),
);
let region_data = get_intensities(&url).await?;
let start_date = start_date + Duration::minutes(1);
let end_date = end_date + Duration::minutes(1);
// format dates
let start_date = start_date.format("%Y-%m-%dT%H:%MZ");
let end_date = end_date.format("%Y-%m-%dT%H:%MZ");

let url = format!("{BASE_URL}/regional/intensity/{start_date}/{end_date}/{path}");
let region_data = get_intensities_for_url(&url).await?;
let mut tuples = to_tuple(region_data)?;
output.append(&mut tuples);
}
Expand All @@ -241,7 +222,7 @@ fn to_tuple(data: RegionData) -> Result<Vec<(NaiveDateTime, i32)>, ApiError> {
Ok(values)
}

pub async fn get_intensities(url: &str) -> Result<RegionData, ApiError> {
async fn get_intensities_for_url(url: &str) -> Result<RegionData, ApiError> {
let client = Client::new();
let response = client.get(url).send().await?;

Expand All @@ -261,7 +242,7 @@ pub async fn get_intensities(url: &str) -> Result<RegionData, ApiError> {
}

/// Retrieves the intensity value from a structure
async fn get_intensity(url: &str) -> Result<i32, ApiError> {
async fn get_intensity_for_url(url: &str) -> Result<i32, ApiError> {
let result = get_instant_data(url).await?;

let intensity = result
Expand Down
62 changes: 8 additions & 54 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
use std::{fmt::Display, process, str::FromStr};
use std::process;

use carbonintensity::{
get_intensities_postcode, get_intensities_region, get_intensity_postcode, get_intensity_region,
ApiError, Region,
};
use carbonintensity::{get_intensities, get_intensity, ApiError, Target};
use chrono::NaiveDateTime;
use clap::Parser;

Expand All @@ -24,28 +21,6 @@ struct Args {
pub value: String,
}

enum Target {
// NATIONAL,
Postcode(String),
Region(Region),
}

impl FromStr for Target {
type Err = ();

fn from_str(s: &str) -> Result<Self, Self::Err> {
//"" => Ok(Target::NATIONAL)

// Check if input can be parsed as a Region
if let Ok(region) = s.parse::<Region>() {
return Ok(Target::Region(region));
}

// Assumes the string was a postcode
Ok(Target::Postcode(s.to_string()))
}
}

#[tokio::main]
async fn main() {
let args = Args::parse();
Expand All @@ -57,27 +32,11 @@ async fn main() {
if let Some(start_date) = &args.start_date {
let end_date: Option<&str> = args.end_date.as_deref();

match target {
Target::Postcode(postcode) => {
let result = get_intensities_postcode(&postcode, start_date, &end_date).await;
handle_results(result);
}
Target::Region(region) => {
let result = get_intensities_region(region, start_date, &end_date).await;
handle_results(result);
}
}
let result = get_intensities(&target, start_date, &end_date).await;
handle_results(result);
} else {
match target {
Target::Postcode(postcode) => {
let result = get_intensity_postcode(&postcode).await;
handle_result(result, &"postcode", &postcode);
}
Target::Region(region) => {
let result = get_intensity_region(region).await;
handle_result(result, &"region", &region);
}
}
let result = get_intensity(&target).await;
handle_result(result, &target);
}
}

Expand All @@ -92,14 +51,9 @@ fn handle_results(result: Result<Vec<(NaiveDateTime, i32)>, ApiError>) {
}
}

fn handle_result(result: Result<i32, ApiError>, method: &dyn Display, value: &dyn Display) {
fn handle_result(result: Result<i32, ApiError>, target: &Target) {
if result.is_ok() {
println!(
"Carbon intensity for {} {}: {:?}",
method,
value,
result.unwrap()
);
println!("Carbon intensity for {}: {:?}", target, result.unwrap());
} else {
eprintln!("{}", result.unwrap_err());
process::exit(1);
Expand Down
37 changes: 37 additions & 0 deletions src/target.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use std::str::FromStr;

use crate::Region;

/// Carbon intensity target, e.g. a postcode or a region
pub enum Target {
// NATIONAL,
Postcode(String),
Region(Region),
}

impl FromStr for Target {
type Err = ();

fn from_str(s: &str) -> Result<Self, Self::Err> {
//"" => Ok(Target::NATIONAL)

// Check if input can be parsed as a Region
if let Ok(region) = s.parse::<Region>() {
return Ok(Target::Region(region));
}

// Assumes the string was a postcode
Ok(Target::Postcode(s.to_string()))
}
}

impl std::fmt::Display for Target {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let target = match self {
Target::Postcode(postcode) => format!("postcode {postcode}"),
Target::Region(region) => region.to_string(),
};

write!(f, "{target}")
}
}

0 comments on commit 031fc5e

Please sign in to comment.