jquants_api_client/api/
earnings_calendar.rs

1//! Earnings Calendar (/fins/announcement) API
2
3use serde::{Deserialize, Serialize};
4
5use super::{
6    shared::{
7        deserialize_utils::empty_string_or_null_as_none,
8        traits::{
9            builder::JQuantsBuilder,
10            pagination::{HasPaginationKey, MergePage, Paginatable},
11        },
12    },
13    JQuantsApiClient, JQuantsPlanClient,
14};
15
16/// Builder for Earnings Calendar Data API.
17#[derive(Clone, Serialize)]
18pub struct EarningsCalendarBuilder {
19    #[serde(skip)]
20    client: JQuantsApiClient,
21
22    /// Pagination key.
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pagination_key: Option<String>,
25}
26
27impl JQuantsBuilder<EarningsCalendarResponse> for EarningsCalendarBuilder {
28    async fn send(self) -> Result<EarningsCalendarResponse, crate::JQuantsError> {
29        self.send_ref().await
30    }
31
32    async fn send_ref(&self) -> Result<EarningsCalendarResponse, crate::JQuantsError> {
33        self.client.inner.get("fins/announcement", self).await
34    }
35}
36
37impl Paginatable<EarningsCalendarResponse> for EarningsCalendarBuilder {
38    fn pagination_key(mut self, pagination_key: impl Into<String>) -> Self {
39        self.pagination_key = Some(pagination_key.into());
40        self
41    }
42}
43
44impl EarningsCalendarBuilder {
45    /// Create a new builder.
46    pub(crate) fn new(client: JQuantsApiClient) -> Self {
47        Self {
48            client,
49            pagination_key: None,
50        }
51    }
52
53    /// Set pagination key for fetching the next set of data.
54    pub fn pagination_key(mut self, pagination_key: impl Into<String>) -> Self {
55        self.pagination_key = Some(pagination_key.into());
56        self
57    }
58}
59
60/// Trait for Earnings Calendar Data API.
61pub trait EarningsCalendarApi: JQuantsPlanClient {
62    /// Get API builder for Earnings Calendar Data.
63    ///
64    /// Use [Earnings Calendar Data (/fins/announcement) API](https://jpx.gitbook.io/j-quants-en/api-reference/announcement)
65    fn get_earnings_calendar(&self) -> EarningsCalendarBuilder {
66        EarningsCalendarBuilder::new(self.get_api_client().clone())
67    }
68}
69
70/// Earnings Calendar Data API response.
71///
72/// See: [API Reference](https://jpx.gitbook.io/j-quants-en/api-reference/announcement)
73#[derive(Debug, Clone, PartialEq, Deserialize)]
74pub struct EarningsCalendarResponse {
75    /// List of earnings announcements
76    pub announcement: Vec<EarningsAnnouncementItem>,
77    /// Pagination key for fetching next set of data
78    pub pagination_key: Option<String>,
79}
80
81impl HasPaginationKey for EarningsCalendarResponse {
82    fn get_pagination_key(&self) -> Option<&str> {
83        self.pagination_key.as_deref()
84    }
85}
86
87impl MergePage for EarningsCalendarResponse {
88    fn merge_page(
89        page: Result<Vec<Self>, crate::JQuantsError>,
90    ) -> Result<Self, crate::JQuantsError> {
91        let mut page = page?;
92        let mut merged = page.pop().unwrap();
93        for p in page {
94            merged.announcement.extend(p.announcement);
95        }
96        merged.pagination_key = None;
97
98        Ok(merged)
99    }
100}
101
102/// Represents a single earnings announcement item.
103#[derive(Debug, Clone, PartialEq, Deserialize)]
104pub struct EarningsAnnouncementItem {
105    /// Announcement Date (YYYY-MM-DD)
106    ///
107    /// If the earnings announcement date is undecided, the data will be an empty string ("").
108    #[serde(rename = "Date", deserialize_with = "empty_string_or_null_as_none")]
109    pub date: Option<String>,
110
111    /// Issue Code (e.g., "43760")
112    #[serde(rename = "Code")]
113    pub code: String,
114
115    /// Company Name (Japanese)
116    #[serde(rename = "CompanyName")]
117    pub company_name: String,
118
119    /// End of Fiscal Year (Japanese, e.g., "9月30日")
120    #[serde(rename = "FiscalYear")]
121    pub fiscal_year: String,
122
123    /// Sector Name (Japanese)
124    #[serde(rename = "SectorName")]
125    pub sector_name: String,
126
127    /// Fiscal Quarter (Japanese, e.g., "第1四半期")
128    #[serde(rename = "FiscalQuarter")]
129    pub fiscal_quarter: String,
130
131    /// Market Segment Name (Japanese, e.g., "マザーズ")
132    #[serde(rename = "Section")]
133    pub section: String,
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_deserialize_earnings_calendar_response() {
142        let json_data = r#"
143        {
144            "announcement": [
145                {
146                    "Date": "2022-02-14",
147                    "Code": "43760",
148                    "CompanyName": "くふうカンパニー",
149                    "FiscalYear": "9月30日",
150                    "SectorName": "情報・通信業",
151                    "FiscalQuarter": "第1四半期",
152                    "Section": "マザーズ"
153                }
154            ],
155            "pagination_key": "value1.value2."
156        }
157        "#;
158
159        let response: EarningsCalendarResponse = serde_json::from_str(json_data).unwrap();
160
161        let expected_announcement = vec![EarningsAnnouncementItem {
162            date: Some("2022-02-14".to_string()),
163            code: "43760".to_string(),
164            company_name: "くふうカンパニー".to_string(),
165            fiscal_year: "9月30日".to_string(),
166            sector_name: "情報・通信業".to_string(),
167            fiscal_quarter: "第1四半期".to_string(),
168            section: "マザーズ".to_string(),
169        }];
170
171        let expected_response = EarningsCalendarResponse {
172            announcement: expected_announcement,
173            pagination_key: Some("value1.value2.".to_string()),
174        };
175
176        pretty_assertions::assert_eq!(response, expected_response);
177    }
178
179    #[test]
180    fn test_deserialize_earnings_calendar_response_no_pagination_key() {
181        let json_data = r#"
182        {
183            "announcement": [
184                {
185                    "Date": "2022-02-14",
186                    "Code": "43760",
187                    "CompanyName": "くふうカンパニー",
188                    "FiscalYear": "9月30日",
189                    "SectorName": "情報・通信業",
190                    "FiscalQuarter": "第1四半期",
191                    "Section": "マザーズ"
192                }
193            ]
194        }
195        "#;
196
197        let response: EarningsCalendarResponse = serde_json::from_str(json_data).unwrap();
198
199        let expected_announcement = vec![EarningsAnnouncementItem {
200            date: Some("2022-02-14".to_string()),
201            code: "43760".to_string(),
202            company_name: "くふうカンパニー".to_string(),
203            fiscal_year: "9月30日".to_string(),
204            sector_name: "情報・通信業".to_string(),
205            fiscal_quarter: "第1四半期".to_string(),
206            section: "マザーズ".to_string(),
207        }];
208
209        let expected_response = EarningsCalendarResponse {
210            announcement: expected_announcement,
211            pagination_key: None,
212        };
213
214        pretty_assertions::assert_eq!(response, expected_response);
215    }
216
217    #[test]
218    fn test_deserialize_earnings_calendar_response_multiple_items() {
219        let json_data = r#"
220        {
221            "announcement": [
222                {
223                    "Date": "2023-03-06",
224                    "Code": "86970",
225                    "CompanyName": "株式会社XYZ",
226                    "FiscalYear": "3月31日",
227                    "SectorName": "製造業",
228                    "FiscalQuarter": "第4四半期",
229                    "Section": "東証プライム"
230                },
231                {
232                    "Date": "2023-03-07",
233                    "Code": "86971",
234                    "CompanyName": "株式会社ABC",
235                    "FiscalYear": "9月30日",
236                    "SectorName": "金融業",
237                    "FiscalQuarter": "第1四半期",
238                    "Section": "東証マザーズ"
239                }
240            ],
241            "pagination_key": "value3.value4."
242        }
243        "#;
244
245        let response: EarningsCalendarResponse = serde_json::from_str(json_data).unwrap();
246
247        let expected_announcement = vec![
248            EarningsAnnouncementItem {
249                date: Some("2023-03-06".to_string()),
250                code: "86970".to_string(),
251                company_name: "株式会社XYZ".to_string(),
252                fiscal_year: "3月31日".to_string(),
253                sector_name: "製造業".to_string(),
254                fiscal_quarter: "第4四半期".to_string(),
255                section: "東証プライム".to_string(),
256            },
257            EarningsAnnouncementItem {
258                date: Some("2023-03-07".to_string()),
259                code: "86971".to_string(),
260                company_name: "株式会社ABC".to_string(),
261                fiscal_year: "9月30日".to_string(),
262                sector_name: "金融業".to_string(),
263                fiscal_quarter: "第1四半期".to_string(),
264                section: "東証マザーズ".to_string(),
265            },
266        ];
267
268        let expected_response = EarningsCalendarResponse {
269            announcement: expected_announcement,
270            pagination_key: Some("value3.value4.".to_string()),
271        };
272
273        pretty_assertions::assert_eq!(response, expected_response);
274    }
275
276    #[test]
277    fn test_deserialize_earnings_calendar_response_no_data() {
278        let json_data = r#"
279        {
280            "announcement": []
281        }
282        "#;
283
284        let response: EarningsCalendarResponse = serde_json::from_str(json_data).unwrap();
285        let expected_response = EarningsCalendarResponse {
286            announcement: vec![],
287            pagination_key: None,
288        };
289
290        pretty_assertions::assert_eq!(response, expected_response);
291    }
292}