Skip to content

Commit e68a6eb

Browse files
committed
feat: add r055
1 parent 277d89d commit e68a6eb

File tree

9 files changed

+248
-1
lines changed

9 files changed

+248
-1
lines changed

docs/cli/indicators/R/055.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# TODO The title of the indicator (R055)
2+
3+
TODO A one-sentence description of the indicator.
4+
5+
## Methodology
6+
7+
TODO
8+
9+
:::{admonition} Example
10+
:class: seealso
11+
12+
TODO
13+
:::
14+
15+
:::{admonition} Why is this a red flag?
16+
:class: hint
17+
18+
TODO
19+
:::
20+
21+
<small>Based on "TODO" in [*TODO*](TODO).</small>
22+
23+
## Output
24+
25+
The indicator's value is TODO.
26+
27+
## Configuration
28+
29+
All configuration is optional. To override the default TODO:
30+
31+
```ini
32+
[R055]
33+
TODO
34+
```
35+
36+
## Exclusions
37+
38+
A contracting process is excluded if:
39+
40+
- TODO
41+
42+
## Assumptions
43+
44+
TODO
45+
46+
## Demonstration
47+
48+
*Input*
49+
50+
:::{literalinclude} ../../../examples/R/055.jsonl
51+
:language: json
52+
:::
53+
54+
*Output*
55+
56+
```console
57+
$ ocdscardinal indicators --settings docs/examples/settings.ini --no-meta docs/examples/R/055.jsonl
58+
{}
59+
60+
```

docs/cli/init.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ $ ocdscardinal init -
118118
; threshold = 10
119119
; minimum_contracting_processes = 20
120120

121+
[R055]
122+
; threshold = 66593
123+
; start_date = 2022-01-01
124+
; end_date = 2022-12-31
125+
121126
[R058]
122127
; threshold = 0.5
123128

docs/examples/R/055.jsonl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

docs/examples/settings.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@
88
[R036]
99
[R038]
1010
[R048]
11+
[R055]
1112
[R058]

src/indicators/mod.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,17 @@ pub mod r035;
88
pub mod r036;
99
pub mod r038;
1010
pub mod r048;
11+
pub mod r055;
1112
pub mod r058;
1213
pub mod util;
1314

1415
use std::collections::{HashMap, HashSet};
1516
use std::ops::AddAssign;
1617

18+
use chrono::NaiveDate;
1719
use indexmap::IndexMap;
1820
use serde::ser::SerializeMap;
19-
use serde::{Deserialize, Serialize, Serializer};
21+
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
2022
use serde_json::{Map, Value};
2123

2224
// Settings.
@@ -119,6 +121,32 @@ pub struct R048 {
119121
pub minimum_contracting_processes: Option<usize>,
120122
}
121123

124+
#[derive(Clone, Debug, Default, Deserialize)]
125+
#[serde(deny_unknown_fields)]
126+
pub struct R055 {
127+
pub threshold: Option<f64>,
128+
#[serde(deserialize_with = "naive_date_from_str")]
129+
pub start_date: Option<NaiveDate>,
130+
#[serde(deserialize_with = "naive_date_from_str")]
131+
pub end_date: Option<NaiveDate>,
132+
}
133+
134+
// https://serde.rs/field-attrs.html#deserialize_with
135+
fn naive_date_from_str<'de, D>(deserializer: D) -> Result<Option<NaiveDate>, D::Error>
136+
where
137+
D: Deserializer<'de>,
138+
{
139+
let s: Option<String> = Option::deserialize(deserializer)?;
140+
s.map_or_else(
141+
|| Ok(None),
142+
|s| {
143+
NaiveDate::parse_from_str(&s, "%Y-%m-%d")
144+
.map(Some)
145+
.map_err(de::Error::custom)
146+
},
147+
)
148+
}
149+
122150
#[derive(Clone, Debug, Default, Deserialize)]
123151
#[serde(deny_unknown_fields)]
124152
#[allow(non_snake_case)]
@@ -144,6 +172,7 @@ pub struct Settings {
144172
pub R036: Option<Empty>,
145173
pub R038: Option<R038>,
146174
pub R048: Option<R048>,
175+
pub R055: Option<R055>,
147176
pub R058: Option<FloatThreshold>, // ratio
148177
}
149178

@@ -169,15 +198,18 @@ pub enum Indicator {
169198
R036,
170199
R038,
171200
R048,
201+
R055,
172202
R058,
173203
}
174204

175205
#[derive(Debug, Default, Serialize)]
176206
pub struct Maps {
177207
/// The buyer for each `ocid` in which at least one bid is disqualified.
178208
pub ocid_buyer_r038: HashMap<String, String>,
209+
pub ocid_buyer_supplier_r055: HashMap<String, String>,
179210
/// The procuring entity for each `ocid` in which at least one bid is disqualified.
180211
pub ocid_procuringentity_r038: HashMap<String, String>,
212+
pub ocid_procuringentity_supplier_r055: HashMap<String, String>,
181213
/// The tenderers that submitted bids for each `ocid`.
182214
pub ocid_tenderer: HashMap<String, HashSet<String>>,
183215
/// The flagged tenderers for each flagged `ocid`.
@@ -216,6 +248,12 @@ pub struct Indicators {
216248
pub r038_tenderer: HashMap<String, Fraction>,
217249
/// The item classifications for each `bids/details/tenderers/id`.
218250
pub r048_classifications: HashMap<String, (usize, HashSet<String>)>,
251+
/// The `tender/value/amount` for each `ocid` when `tender/procurementMethod` is 'open'.
252+
pub r055_open_tender_amount: HashMap<String, f64>,
253+
/// The total awarded amount for each `buyer/id` and `awards/suppliers/id` when `tender/procurementMethod` is 'direct'.
254+
pub r055_direct_awarded_amount_supplier_buyer: HashMap<(String, String), f64>,
255+
/// The total awarded amount for each `tender/procuringEntity/id` and `awards/suppliers/id` when `tender/procurementMethod` is 'direct'.
256+
pub r055_direct_awarded_amount_supplier_procuring_entity: HashMap<(String, String), f64>,
219257
/// Whether to map contracting processes to organizations.
220258
pub map: bool,
221259
}

src/indicators/r055.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
use chrono::{Duration, NaiveDate, Utc};
2+
use log::warn;
3+
use serde_json::{Map, Value};
4+
5+
use crate::indicators::{set_meta, set_result, sum, Calculate, Indicators, Settings};
6+
7+
#[derive(Default)]
8+
pub struct R055 {
9+
threshold: Option<f64>, // resolved in finalize()
10+
start_date: NaiveDate,
11+
end_date: NaiveDate,
12+
currency: Option<String>,
13+
}
14+
15+
impl Calculate for R055 {
16+
fn new(settings: &mut Settings) -> Self {
17+
let setting = std::mem::take(&mut settings.R055).unwrap_or_default();
18+
let today = Utc::now().date_naive();
19+
20+
Self {
21+
threshold: setting.threshold,
22+
start_date: setting.start_date.unwrap_or(today - Duration::days(365)),
23+
end_date: setting.end_date.unwrap_or(today),
24+
currency: settings.currency.clone(),
25+
}
26+
}
27+
28+
fn fold(&self, item: &mut Indicators, release: &Map<String, Value>, ocid: &str) {
29+
if let Some(Value::Object(tender)) = release.get("tender")
30+
&& let Some(Value::Object(tender_period)) = tender.get("tenderPeriod")
31+
&& let Some(Value::String(date)) = tender_period.get("startDate")
32+
&& let Some(Value::String(method)) = tender.get("procurementMethod")
33+
&& method == "open"
34+
&& let Ok(date) = NaiveDate::parse_from_str(date, "%Y-%m-%dT%H:%M:%S%z")
35+
&& date >= self.start_date
36+
&& date <= self.end_date
37+
&& let Some(Value::Object(value)) = tender.get("value")
38+
&& let Some(Value::Number(amount)) = value.get("amount")
39+
&& let Some(Value::String(currency)) = value.get("currency")
40+
&& let Some(amount) = amount.as_f64()
41+
{
42+
if currency
43+
== item
44+
.currency
45+
.get_or_insert_with(|| self.currency.as_ref().map_or_else(|| currency.clone(), Clone::clone))
46+
{
47+
item.r055_open_tender_amount.insert(ocid.to_owned(), amount);
48+
} else {
49+
warn!("{} is not {:?}, skipping.", currency, item.currency);
50+
}
51+
}
52+
53+
if let Some(Value::Array(awards)) = release.get("awards") {
54+
for award in awards {
55+
if let Some(Value::String(status)) = award.get("status")
56+
&& let Some(Value::Array(suppliers)) = award.get("suppliers")
57+
&& suppliers.len() == 1
58+
&& let Some(Value::String(supplier_id)) = suppliers[0].get("id")
59+
&& status == "active"
60+
&& let Some(Value::String(date)) = award.get("date")
61+
&& let Ok(date) = NaiveDate::parse_from_str(date, "%Y-%m-%dT%H:%M:%S%z")
62+
&& date >= self.start_date
63+
&& date <= self.end_date
64+
&& let Some(Value::Object(value)) = award.get("value")
65+
&& let Some(Value::Number(amount)) = value.get("amount")
66+
&& let Some(Value::String(currency)) = value.get("currency")
67+
&& let Some(amount) = amount.as_f64()
68+
{
69+
if currency
70+
== item.currency.get_or_insert_with(|| {
71+
self.currency.as_ref().map_or_else(|| currency.clone(), Clone::clone)
72+
})
73+
{
74+
if let Some(Value::Object(buyer)) = release.get("buyer")
75+
&& let Some(Value::String(id)) = buyer.get("id")
76+
{
77+
item.r055_direct_awarded_amount_supplier_buyer
78+
.insert((id.clone(), supplier_id.clone()), amount);
79+
}
80+
if let Some(Value::Object(tender)) = release.get("tender")
81+
&& let Some(Value::Object(procuring_entity)) = tender.get("procuringEntity")
82+
&& let Some(Value::String(id)) = procuring_entity.get("id")
83+
{
84+
item.r055_direct_awarded_amount_supplier_procuring_entity
85+
.insert((id.clone(), supplier_id.clone()), amount);
86+
}
87+
} else {
88+
warn!("{} is not {:?}, skipping.", currency, item.currency);
89+
}
90+
}
91+
}
92+
}
93+
}
94+
95+
fn reduce(&self, item: &mut Indicators, other: &mut Indicators) {
96+
let mut min = 0.0;
97+
for (key, value) in std::mem::take(&mut other.r055_open_tender_amount) {
98+
if min <= value {
99+
item.r055_open_tender_amount.insert(key, value);
100+
min = value;
101+
}
102+
}
103+
sum!(item, other, r055_direct_awarded_amount_supplier_buyer);
104+
sum!(item, other, r055_direct_awarded_amount_supplier_procuring_entity);
105+
}
106+
107+
fn finalize(&self, item: &mut Indicators) {
108+
let min_amount = self.threshold.map_or_else(
109+
|| {
110+
std::mem::take(&mut item.r055_open_tender_amount)
111+
.values()
112+
.copied()
113+
.last()
114+
.unwrap_or(0.0)
115+
},
116+
|v| v,
117+
);
118+
set_meta!(item, R055, "lower_open_amount", min_amount);
119+
120+
for (id, amount) in &item.r055_direct_awarded_amount_supplier_buyer {
121+
if *amount >= min_amount {
122+
set_result!(item, Buyer, id.0, R055, *amount);
123+
set_result!(item, Tenderer, id.1, R055, *amount);
124+
}
125+
}
126+
for (id, amount) in &item.r055_direct_awarded_amount_supplier_procuring_entity {
127+
if *amount >= min_amount {
128+
set_result!(item, ProcuringEntity, id.0, R055, *amount);
129+
set_result!(item, Tenderer, id.1, R055, *amount);
130+
}
131+
}
132+
}
133+
}

src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use crate::indicators::r035::R035;
2727
use crate::indicators::r036::R036;
2828
use crate::indicators::r038::R038;
2929
use crate::indicators::r048::R048;
30+
use crate::indicators::r055::R055;
3031
use crate::indicators::r058::R058;
3132
use crate::indicators::util::{SecondLowestBidRatio, Tenderers};
3233
pub use crate::indicators::{Calculate, Codelist, Exclusions, Group, Indicator, Indicators, Modifications, Settings};
@@ -144,6 +145,11 @@ pub fn init(path: &PathBuf, force: &bool) -> std::io::Result<bool> {
144145
; threshold = 10
145146
; minimum_contracting_processes = 20
146147
148+
[R055]
149+
; threshold = 66593
150+
; start_date = 2022-01-01
151+
; end_date = 2022-12-31
152+
147153
[R058]
148154
; threshold = 0.5
149155
";
@@ -255,6 +261,7 @@ impl Indicators {
255261
R036,
256262
R038,
257263
R048,
264+
R055,
258265
R058,
259266
);
260267

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

tests/fixtures/indicators/R055.jsonl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

0 commit comments

Comments
 (0)