diff --git a/crates/fmp-py/python/fmp/_native/indexes/__init__.pyi b/crates/fmp-py/python/fmp/_native/indexes/__init__.pyi index 01c31d1b..cad7e343 100644 --- a/crates/fmp-py/python/fmp/_native/indexes/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/indexes/__init__.pyi @@ -93,7 +93,7 @@ class IndexConstituent: @property def cik(self) -> builtins.str: ... @property - def founded(self) -> datetime.date: ... + def founded(self) -> builtins.str: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... def __new__( cls, @@ -105,7 +105,7 @@ class IndexConstituent: head_quarter: builtins.str, date_first_added: typing.Optional[datetime.date], cik: builtins.str, - founded: datetime.date, + founded: builtins.str, ) -> IndexConstituent: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... def __repr__(self) -> builtins.str: ... diff --git a/crates/fmp-py/src/models/indexes.rs b/crates/fmp-py/src/models/indexes.rs index 68c1a529..4928e5f2 100644 --- a/crates/fmp-py/src/models/indexes.rs +++ b/crates/fmp-py/src/models/indexes.rs @@ -130,7 +130,7 @@ pub(crate) struct IndexConstituent { #[pyo3(get)] pub cik: String, #[pyo3(get)] - pub founded: ::chrono::NaiveDate, + pub founded: String, } #[gen_stub_pymethods] @@ -148,7 +148,7 @@ impl IndexConstituent { head_quarter: String, date_first_added: Option<::chrono::NaiveDate>, cik: String, - founded: ::chrono::NaiveDate, + founded: String, ) -> Self { Self { symbol, @@ -279,7 +279,7 @@ impl From for IndexConstituent { head_quarter: value.head_quarter, date_first_added: value.date_first_added.map(|value| value.into_inner()), cik: value.cik.into_inner(), - founded: value.founded.into_inner(), + founded: value.founded, } } } diff --git a/crates/fmp-py/tests/test_indexes_b.py b/crates/fmp-py/tests/test_indexes_b.py index 4f297719..db4b2ef7 100644 --- a/crates/fmp-py/tests/test_indexes_b.py +++ b/crates/fmp-py/tests/test_indexes_b.py @@ -23,7 +23,7 @@ "/sp500-constituent", "indexes_sp500_constituents.json", "HONA", - datetime.date(1913, 12, 31), + "1902/1985", datetime.date(2026, 6, 29), id="sp500", ), @@ -32,7 +32,7 @@ "/nasdaq-constituent", "indexes_nasdaq_constituents.json", "ADBE", - datetime.date(1982, 12, 1), + "1982-12-01", None, id="nasdaq", ), @@ -41,7 +41,7 @@ "/dowjones-constituent", "indexes_dow_jones_constituents.json", "GOOGL", - datetime.date(1998, 9, 4), + "1998-09-04", datetime.date(2026, 6, 29), id="dow-jones", ), @@ -54,7 +54,7 @@ def test_current_constituents_take_no_arguments( path: str, fixture: str, symbol: str, - founded: datetime.date, + founded: str, date_first_added: datetime.date | None, ) -> None: """Each current-constituent method hits its own path with no query and decodes the date fields.""" @@ -72,6 +72,18 @@ def test_current_constituents_take_no_arguments( assert row.head_quarter +@pytest.mark.parametrize("founded", ["1994", "1902/1985", "1902/1985/2001"]) +def test_constituent_founded_keeps_the_year_forms_verbatim( + client: Any, fixture_server: FixtureServer, founded: str +) -> None: + """``founded`` is the provider's founding year or years, returned as the exact string.""" + body = load_fixture("indexes_sp500_constituents.json") + body[0]["founded"] = founded + fixture_server.route("/sp500-constituent", body) + + assert client.indexes.sp500_constituents()[0].founded == founded + + @pytest.mark.parametrize( ("method", "path", "fixture", "symbol", "removed_ticker", "date"), [ diff --git a/crates/fmp-py/tests/typecheck_contract.py b/crates/fmp-py/tests/typecheck_contract.py index 7f20b1f5..d7d52c9a 100644 --- a/crates/fmp-py/tests/typecheck_contract.py +++ b/crates/fmp-py/tests/typecheck_contract.py @@ -238,7 +238,7 @@ def check_indexes_contract(client: FmpClient) -> None: bar_time: datetime.datetime = intraday[0].date members: list[IndexConstituent] = client.indexes.sp500_constituents() first_added: datetime.date | None = members[0].date_first_added - founded: datetime.date = members[0].founded + founded: str = members[0].founded _ = (currency, bar_date, vwap, bar_time, first_added, founded) diff --git a/crates/libfmp/src/responses/indexes.rs b/crates/libfmp/src/responses/indexes.rs index 20b6d9ca..7f7f7e3c 100644 --- a/crates/libfmp/src/responses/indexes.rs +++ b/crates/libfmp/src/responses/indexes.rs @@ -42,7 +42,10 @@ pub struct IndexConstituent { #[serde(deserialize_with = "required_option")] pub date_first_added: Option, pub cik: Cik, - pub founded: Date, + /// Founding year or years as the provider sends them, kept verbatim: a + /// year (`"1994"`), several years joined by `/` (`"1902/1985"`), or a full + /// date (`"1982-12-01"`) on some indexes. It is not a date. + pub founded: String, } /// One documented historical addition to and removal from a major US index. diff --git a/crates/libfmp/tests/fixtures/indexes_sp500_constituents.json b/crates/libfmp/tests/fixtures/indexes_sp500_constituents.json index e143239c..36c0a145 100644 --- a/crates/libfmp/tests/fixtures/indexes_sp500_constituents.json +++ b/crates/libfmp/tests/fixtures/indexes_sp500_constituents.json @@ -2,7 +2,7 @@ { "cik": "0002089271", "dateFirstAdded": "2026-06-29", - "founded": "1913-12-31", + "founded": "1902/1985", "headQuarter": "Charlotte, North Carolina", "name": "Honeywell Aerospace Inc", "sector": "Technology", diff --git a/crates/libfmp/tests/indexes_constituent_responses.rs b/crates/libfmp/tests/indexes_constituent_responses.rs index 5e066f1e..06034485 100644 --- a/crates/libfmp/tests/indexes_constituent_responses.rs +++ b/crates/libfmp/tests/indexes_constituent_responses.rs @@ -29,9 +29,11 @@ fn all_six_documented_constituent_fixtures_match_the_typed_wire_contract_exactly current[0].date_first_added.unwrap().to_string(), "2026-06-29" ); + assert_eq!(current[0].founded, "1902/1985"); let nullable: Vec = serde_json::from_slice(NASDAQ).unwrap(); assert_eq!(nullable[0].cik.as_str(), "0000796343"); + assert_eq!(nullable[0].founded, "1982-12-01"); assert_eq!(nullable[0].date_first_added, None); let history: Vec = @@ -98,6 +100,18 @@ fn current_constituent_fields_are_required_and_nullable_date_stays_required() { assert_eq!(decoded.symbol.as_str(), "ADBE"); } +#[test] +fn founded_keeps_year_multi_year_and_date_forms_verbatim() { + let source: Value = serde_json::from_slice(NASDAQ).unwrap(); + for founded in ["1994", "1902/1985", "1902/1985/2001", "1982-12-01", ""] { + let mut row = source[0].clone(); + row["founded"] = json!(founded); + let decoded: IndexConstituent = serde_json::from_value(row.clone()).unwrap(); + assert_eq!(decoded.founded, founded); + assert_eq!(serde_json::to_value(decoded).unwrap(), row); + } +} + #[test] fn historical_fields_are_required_while_omittable_members_accept_null_or_empty() { let source: Value = serde_json::from_slice(HISTORICAL_NASDAQ).unwrap(); diff --git a/sdk/go/indexes_models.go b/sdk/go/indexes_models.go index a9ebcdd0..f4e22b59 100644 --- a/sdk/go/indexes_models.go +++ b/sdk/go/indexes_models.go @@ -66,7 +66,7 @@ type IndexConstituent struct { HeadQuarter string `json:"headQuarter"` DateFirstAdded *Date `json:"dateFirstAdded"` CIK string `json:"cik"` - Founded Date `json:"founded"` + Founded string `json:"founded"` } // indexConstituentShadow mirrors IndexConstituent with a pointer or raw value @@ -80,7 +80,7 @@ type indexConstituentShadow struct { HeadQuarter *string `json:"headQuarter"` DateFirstAdded jsontext.Value `json:"dateFirstAdded"` CIK *string `json:"cik"` - Founded *Date `json:"founded"` + Founded *string `json:"founded"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode diff --git a/sdk/go/indexes_parity_test.go b/sdk/go/indexes_parity_test.go index c83868b5..3752fcb8 100644 --- a/sdk/go/indexes_parity_test.go +++ b/sdk/go/indexes_parity_test.go @@ -108,7 +108,7 @@ func TestDocumentedIndexConstituentFixturesDecodeExactValues(t *testing.T) { t.Parallel() sp500 := assertFixtureParity[IndexConstituent](t, "indexes_sp500_constituents.json") if len(sp500) != 1 || sp500[0].Symbol != "HONA" || sp500[0].CIK != "0002089271" || - sp500[0].SubSector != "Aerospace & Defense" || sp500[0].Founded != mustParseDate(t, "1913-12-31") { + sp500[0].SubSector != "Aerospace & Defense" || sp500[0].Founded != "1902/1985" { t.Fatalf("indexes_sp500_constituents = %+v", sp500) } if got := sp500[0].DateFirstAdded; got == nil || got.String() != "2026-06-29" { @@ -117,7 +117,7 @@ func TestDocumentedIndexConstituentFixturesDecodeExactValues(t *testing.T) { nasdaq := assertFixtureParity[IndexConstituent](t, "indexes_nasdaq_constituents.json") if len(nasdaq) != 1 || nasdaq[0].Symbol != "ADBE" || nasdaq[0].CIK != "0000796343" || - nasdaq[0].HeadQuarter != "San Jose, CA" { + nasdaq[0].HeadQuarter != "San Jose, CA" || nasdaq[0].Founded != "1982-12-01" { t.Fatalf("indexes_nasdaq_constituents = %+v", nasdaq) } if nasdaq[0].DateFirstAdded != nil { @@ -172,6 +172,12 @@ func TestIndexesRequiredMembersAreEnforcedLikeSerde(t *testing.T) { if err := json.Unmarshal([]byte(wire), &rows); err != nil || len(rows) != 1 || rows[0].DateFirstAdded != nil { t.Fatalf("null dateFirstAdded with unknown member: %+v, %v", rows, err) } + for _, founded := range []string{"1994", "1902/1985", "1902/1985/2001"} { + wire = `[{"dateFirstAdded":null,` + strings.Replace(constituent, "1982-12-01", founded, 1) + `}]` + if err := json.Unmarshal([]byte(wire), &rows); err != nil || len(rows) != 1 || rows[0].Founded != founded { + t.Fatalf("founded %q: %+v, %v", founded, rows, err) + } + } var changes []HistoricalIndexConstituent wire = `[{"removedSecurity":null,"removedTicker":null,` + history + `,"futureField":[1,2,3]}]` if err := json.Unmarshal([]byte(wire), &changes); err != nil || len(changes) != 1 ||