use std::cmp::Ordering;
use std::fmt::{Debug, Display, Formatter};
use std::num::NonZero;
use crate::errors::FIPSError;
use crate::states::USState;
use crate::{
CountyCode, DataCode, IdCode, SettingCategoryCode, StateCode, TractCode, CATEGORY_OFFSET,
COUNTY_OFFSET, FOURTEEN_BIT_MASK, FOUR_BIT_MASK, ID_OFFSET, NINE_BIT_MASK, STATE_OFFSET,
TEN_BIT_MASK, TRACT_OFFSET, TWENTY_BIT_MASK,
};
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct FIPSCode(NonZero<u64>);
impl FIPSCode {
#[must_use]
pub fn with_state(state: USState) -> Self {
Self::new(state.into(), 0, 0, 0, 0, 0).unwrap()
}
pub fn with_state_code(state_code: StateCode) -> Result<Self, FIPSError> {
Self::new(state_code, 0, 0, 0, 0, 0)
}
pub fn with_county(state: StateCode, county: CountyCode) -> Result<Self, FIPSError> {
Self::new(state, county, 0, 0, 0, 0)
}
pub fn with_tract(
state: StateCode,
county: CountyCode,
tract: TractCode,
) -> Result<Self, FIPSError> {
Self::new(state, county, tract, 0, 0, 0)
}
pub fn with_category(
state: StateCode,
county: CountyCode,
tract: TractCode,
category: SettingCategoryCode,
) -> Result<Self, FIPSError> {
Self::new(state, county, tract, category, 0, 0)
}
pub fn new(
state: StateCode,
county: CountyCode,
tract: TractCode,
category: SettingCategoryCode,
id: IdCode,
data: DataCode,
) -> Result<Self, FIPSError> {
let encoded: u64 = Self::encode_state(state)?
| Self::encode_county(county)?
| Self::encode_tract(tract)?
| Self::encode_category(category)?
| Self::encode_id(id)?
| Self::encode_data(data)?;
let encoded = NonZero::new(encoded).unwrap();
Ok(Self(encoded))
}
#[inline(always)]
pub fn state(&self) -> Result<USState, FIPSError> {
USState::decode(self.state_code())
}
#[inline(always)]
#[must_use]
pub fn state_code(&self) -> StateCode {
(self.0.get() >> STATE_OFFSET) as StateCode
}
#[inline(always)]
#[must_use]
pub fn county_code(&self) -> CountyCode {
((self.0.get() >> COUNTY_OFFSET) as CountyCode) & TEN_BIT_MASK
}
#[inline(always)]
#[must_use]
pub fn census_tract_code(&self) -> TractCode {
((self.0.get() >> TRACT_OFFSET) as TractCode) & TWENTY_BIT_MASK
}
#[inline(always)]
#[must_use]
pub fn category_code(&self) -> SettingCategoryCode {
((self.0.get() >> CATEGORY_OFFSET) as SettingCategoryCode) & FOUR_BIT_MASK
}
#[inline(always)]
#[must_use]
pub fn id(&self) -> IdCode {
((self.0.get() >> ID_OFFSET) as IdCode) & FOURTEEN_BIT_MASK
}
#[inline(always)]
#[must_use]
pub fn data(&self) -> DataCode {
self.0.get() as DataCode & NINE_BIT_MASK
}
#[must_use]
pub fn set_state(&self, state: USState) -> Self {
self.set_state_code(state.into()).unwrap()
}
pub fn set_state_code(&self, state_code: StateCode) -> Result<Self, FIPSError> {
let mut expanded = ExpandedFIPSCode::from_fips_code(*self);
expanded.state = state_code;
expanded.to_fips_code()
}
pub fn set_county(&self, county: CountyCode) -> Result<Self, FIPSError> {
let mut expanded = ExpandedFIPSCode::from_fips_code(*self);
expanded.county = county;
expanded.to_fips_code()
}
pub fn set_tract(&self, tract: TractCode) -> Result<Self, FIPSError> {
let mut expanded = ExpandedFIPSCode::from_fips_code(*self);
expanded.tract = tract;
expanded.to_fips_code()
}
pub fn set_category(&self, category: SettingCategoryCode) -> Result<Self, FIPSError> {
let mut expanded = ExpandedFIPSCode::from_fips_code(*self);
expanded.category = category;
expanded.to_fips_code()
}
pub fn set_id(&self, id: IdCode) -> Result<Self, FIPSError> {
let mut expanded = ExpandedFIPSCode::from_fips_code(*self);
expanded.id = id;
expanded.to_fips_code()
}
pub fn set_data(&self, data: DataCode) -> Result<Self, FIPSError> {
let mut expanded = ExpandedFIPSCode::from_fips_code(*self);
expanded.data = data;
expanded.to_fips_code()
}
#[inline(always)]
pub fn set_data_in_place(&mut self, data: DataCode) -> Result<(), FIPSError> {
if data <= NINE_BIT_MASK {
let inverse_mask = !(NINE_BIT_MASK as u64);
let code = (self.0.get() & inverse_mask) | ((data & NINE_BIT_MASK) as u64);
self.0 = NonZero::new(code).unwrap();
Ok(())
} else {
Err(FIPSError::from_data_code(data))
}
}
#[inline(always)]
#[must_use]
pub fn compare_non_data(&self, other: Self) -> Ordering {
let inverse_mask = !(NINE_BIT_MASK as u64);
let this = self.0.get() & inverse_mask;
let other = other.0.get() & inverse_mask;
this.cmp(&other)
}
#[inline(always)]
fn encode_state(state: StateCode) -> Result<u64, FIPSError> {
if state <= 99 && state != 0 {
Ok((state as u64) << STATE_OFFSET)
} else {
Err(FIPSError::from_state_code(state))
}
}
#[inline(always)]
fn encode_county(county: CountyCode) -> Result<u64, FIPSError> {
if county <= 999 {
Ok((county as u64) << COUNTY_OFFSET)
} else {
Err(FIPSError::from_county_code(county))
}
}
#[inline(always)]
fn encode_tract(tract: TractCode) -> Result<u64, FIPSError> {
if tract <= 999_999 {
Ok((tract as u64) << TRACT_OFFSET)
} else {
Err(FIPSError::from_tract_code(tract))
}
}
#[inline(always)]
fn encode_category(setting_category: SettingCategoryCode) -> Result<u64, FIPSError> {
if setting_category <= FOUR_BIT_MASK {
Ok((setting_category as u64) << CATEGORY_OFFSET)
} else {
Err(FIPSError::from_setting_category_code(setting_category))
}
}
#[inline(always)]
fn encode_id(id: IdCode) -> Result<u64, FIPSError> {
if id <= FOURTEEN_BIT_MASK {
Ok((id as u64) << ID_OFFSET)
} else {
Err(FIPSError::from_id_code(id))
}
}
#[inline(always)]
fn encode_data(data: DataCode) -> Result<u64, FIPSError> {
if data <= NINE_BIT_MASK {
Ok(data as u64)
} else {
Err(FIPSError::from_data_code(data))
}
}
}
impl Display for FIPSCode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", ExpandedFIPSCode::from_fips_code(*self))
}
}
impl Debug for FIPSCode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:02}", self.state_code())?;
write!(f, "-{:03}", self.county_code())?;
write!(f, "-{:06}", self.census_tract_code())?;
write!(f, "-{:01}", self.category_code())?;
write!(f, "-{:05}", self.id())?;
write!(f, "-{:03x}", self.data())?;
Ok(())
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct ExpandedFIPSCode {
pub state: StateCode,
pub county: CountyCode,
pub tract: TractCode,
pub category: SettingCategoryCode,
pub id: IdCode,
pub data: DataCode,
}
impl ExpandedFIPSCode {
#[must_use]
pub fn from_fips_code(fips_code: FIPSCode) -> Self {
Self {
state: fips_code.state_code(),
county: fips_code.county_code(),
tract: fips_code.census_tract_code(),
category: fips_code.category_code(),
id: fips_code.id(),
data: fips_code.data(),
}
}
pub fn to_fips_code(&self) -> Result<FIPSCode, FIPSError> {
FIPSCode::new(
self.state,
self.county,
self.tract,
self.category,
self.id,
self.data,
)
}
}
impl Display for ExpandedFIPSCode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Ok(state) = USState::decode(self.state) {
write!(f, "state: {}", state.as_ref())?;
} else {
write!(f, "state: {}", self.state)?;
}
if self.county != 0 {
write!(f, ", county: {}", self.county)?;
}
if self.tract != 0 {
write!(f, ", tract: {}", self.tract)?;
}
if self.category != 0 {
write!(f, ", setting: {}", self.category)?;
}
if self.id != 0 {
write!(f, ", id: {}", self.id)?;
}
if self.data != 0 {
write!(f, ", data field: {}", self.data)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[repr(u8)]
enum SettingCategory {
Unspecified = 0,
Home,
School,
Work,
CensusTract,
}
impl From<SettingCategory> for SettingCategoryCode {
fn from(value: SettingCategory) -> Self {
value as SettingCategoryCode
}
}
#[test]
fn test_data_ranges() {
assert!(FIPSCode::encode_state(99).is_ok());
assert!(FIPSCode::encode_state(100).is_err());
assert!(FIPSCode::encode_county(999).is_ok());
assert!(FIPSCode::encode_county(1000).is_err());
assert!(FIPSCode::encode_tract(999_999).is_ok());
assert!(FIPSCode::encode_tract(1_000_000).is_err());
assert!(FIPSCode::encode_category(FOUR_BIT_MASK).is_ok());
assert!(FIPSCode::encode_category(FOUR_BIT_MASK + 1).is_err());
assert!(FIPSCode::encode_id(FOURTEEN_BIT_MASK).is_ok());
assert!(FIPSCode::encode_id(FOURTEEN_BIT_MASK + 1).is_err());
assert!(FIPSCode::encode_data(NINE_BIT_MASK).is_ok());
assert!(FIPSCode::encode_data(NINE_BIT_MASK + 1).is_err());
assert!(FIPSCode::with_state_code(1).is_ok());
assert!(FIPSCode::with_state_code(0).is_err());
assert!(FIPSCode::with_state_code(100).is_err());
assert!(FIPSCode::with_county(1, 0).is_ok());
assert!(FIPSCode::with_county(1, 1000).is_err());
assert!(FIPSCode::with_tract(1, 0, 0).is_ok());
assert!(FIPSCode::with_tract(1, 0, 1_000_000).is_err());
assert!(FIPSCode::with_category(1, 0, 0, 0).is_ok());
assert!(FIPSCode::with_category(1, 0, 0, FOUR_BIT_MASK + 1).is_err());
}
#[test]
fn fields_round_trip() {
let fips_code = FIPSCode::new(
USState::TX.into(),
123,
990101,
SettingCategory::Home.into(),
14938,
123,
)
.unwrap();
assert_eq!(fips_code.state().unwrap(), USState::TX);
assert_eq!(fips_code.county_code(), 123);
assert_eq!(fips_code.census_tract_code(), 990101);
assert_eq!(fips_code.category_code(), SettingCategory::Home.into());
assert_eq!(fips_code.id(), 14938);
assert_eq!(fips_code.data(), 123);
}
#[test]
fn expanded_round_trip() {
let fips_code = FIPSCode::new(
USState::TX.into(),
123,
990101,
SettingCategory::Home.into(),
14938,
0x01ff,
)
.unwrap();
let expanded = ExpandedFIPSCode::from_fips_code(fips_code);
let result = expanded.to_fips_code().unwrap();
assert_eq!(result, fips_code);
}
#[test]
fn test_compare_non_data() {
let fips_code_a = FIPSCode::new(
USState::TX.into(),
123,
990101,
SettingCategory::Home.into(),
14938,
0x01ff,
)
.unwrap();
let fips_code_b = FIPSCode::new(
USState::TX.into(),
123,
990101,
SettingCategory::Home.into(),
14938,
0x00ff,
)
.unwrap();
assert_eq!(fips_code_a.compare_non_data(fips_code_b), Ordering::Equal);
assert_eq!(fips_code_a.cmp(&fips_code_b), Ordering::Greater);
}
#[test]
fn test_set_id() {
let fips_code = FIPSCode::with_category(
USState::AK.into(),
0,
0,
SettingCategory::CensusTract.into(),
)
.unwrap();
let other_fips_code = fips_code.set_id(0).unwrap();
assert_eq!(fips_code, other_fips_code);
}
#[test]
fn test_fips_error() {
let err = FIPSError::new("foo", 1, 2, 3);
assert_eq!(
format!("{}", err),
"value 1 provided for foo is outside valid range of 2..3"
);
let e = USState::decode(58).unwrap_err();
assert_eq!(
e.to_string(),
"value 58 provided for USState Code is outside valid range of 1..57"
);
let e = FIPSCode::encode_state(100).unwrap_err();
assert_eq!(
e.to_string(),
"value 100 provided for StateCode is outside valid range of 1..100"
);
let e = FIPSCode::encode_state(0).unwrap_err();
assert_eq!(
e.to_string(),
"value 0 provided for StateCode is outside valid range of 1..100"
);
let e = FIPSCode::encode_county(1000).unwrap_err();
assert_eq!(
e.to_string(),
"value 1000 provided for CountyCode is outside valid range of 0..1000"
);
let e = FIPSCode::encode_tract(1_000_000).unwrap_err();
assert_eq!(
e.to_string(),
"value 1000000 provided for TractCode is outside valid range of 0..1000000"
);
let e = FIPSCode::encode_category(16).unwrap_err();
assert_eq!(
e.to_string(),
"value 16 provided for SettingCategoryCode is outside valid range of 0..16"
);
let e = FIPSCode::encode_id(16_384).unwrap_err();
assert_eq!(
e.to_string(),
"value 16384 provided for IdCode is outside valid range of 0..16384"
);
let e = FIPSCode::encode_data(512).unwrap_err();
assert_eq!(
e.to_string(),
"value 512 provided for DataCode is outside valid range of 0..512"
);
}
}