diff --git a/crates/polars-core/src/chunked_array/cast.rs b/crates/polars-core/src/chunked_array/cast.rs index 08c08bae7d17..47ad2cbd4c4f 100644 --- a/crates/polars-core/src/chunked_array/cast.rs +++ b/crates/polars-core/src/chunked_array/cast.rs @@ -234,8 +234,9 @@ impl ChunkCast for StringChunked { .into_series()) }, (None, None) => self.to_decimal(100), - _ => { - polars_bail!(ComputeError: "expected 'precision' or 'scale' when casting to Decimal") + (_, None) => { + polars_warn!("when inferring scale in string->decimal cast, precision is currently ignored"); + self.to_decimal(100) }, }, #[cfg(feature = "dtype-date")] diff --git a/crates/polars-core/src/series/any_value.rs b/crates/polars-core/src/series/any_value.rs index 5801d1112608..a724f3c81c47 100644 --- a/crates/polars-core/src/series/any_value.rs +++ b/crates/polars-core/src/series/any_value.rs @@ -51,6 +51,15 @@ fn any_values_to_decimal( 0 // integers are treated as decimals with scale of zero } else if let AnyValue::Decimal(_, scale) = av { *scale + } else if matches!(av, AnyValue::Float32(_) | AnyValue::Float64(_)) { + match scale { + Some(s) => s, + None => { + polars_bail!( + ComputeError: "conversion of any-value of dtype {} to decimal must specify scale", av.dtype(), + ); + }, + } } else if matches!(av, AnyValue::Null) { continue; } else { @@ -87,6 +96,11 @@ fn any_values_to_decimal( ) } else if let AnyValue::Decimal(v, scale) = av { (*v, *scale) + } else if matches!(av, AnyValue::Float32(_) | AnyValue::Float64(_)) { + let vf = av.try_extract::().unwrap_or_else(|_| unreachable!()); + let v = ((10f64.powi(scale as i32) * vf).round()) as i128; + // TODO: check + (v, scale) } else { // it has to be a null because we've already checked it builder.append_null(); diff --git a/py-polars/polars/datatypes/classes.py b/py-polars/polars/datatypes/classes.py index 32c974bef404..a7b535f98fc3 100644 --- a/py-polars/polars/datatypes/classes.py +++ b/py-polars/polars/datatypes/classes.py @@ -342,12 +342,12 @@ class Decimal(NumericType): """ precision: int | None - scale: int + scale: int | None def __init__( self, precision: int | None = None, - scale: int = 0, + scale: int | None = None, ): self.precision = precision self.scale = scale diff --git a/py-polars/polars/datatypes/constructor.py b/py-polars/polars/datatypes/constructor.py index 14c79b7d7acf..bf80254d5b3b 100644 --- a/py-polars/polars/datatypes/constructor.py +++ b/py-polars/polars/datatypes/constructor.py @@ -32,7 +32,6 @@ dt.UInt16: PySeries.new_opt_u16, dt.UInt32: PySeries.new_opt_u32, dt.UInt64: PySeries.new_opt_u64, - dt.Decimal: PySeries.new_decimal, dt.Date: PySeries.new_opt_i32, dt.Datetime: PySeries.new_opt_i64, dt.Duration: PySeries.new_opt_i64, @@ -57,6 +56,12 @@ def polars_type_to_constructor( # upon construction if base_type == dt.Array: return functools.partial(PySeries.new_array, dtype.width, dtype.inner) # type: ignore[union-attr] + if base_type == dt.Decimal: + return functools.partial( + PySeries.new_decimal, + getattr(dtype, "precision", None), + getattr(dtype, "scale", None), + ) return _POLARS_TYPE_TO_CONSTRUCTOR[base_type] except KeyError: # pragma: no cover @@ -143,7 +148,7 @@ def numpy_type_to_constructor(dtype: type[np.dtype[Any]]) -> Callable[..., PySer int: PySeries.new_opt_i64, str: PySeries.new_str, bool: PySeries.new_opt_bool, - PyDecimal: PySeries.new_decimal, + PyDecimal: functools.partial(PySeries.new_decimal, None, None), } diff --git a/py-polars/polars/expr/string.py b/py-polars/polars/expr/string.py index 8b07101c8db5..1ba0f1b848f9 100644 --- a/py-polars/polars/expr/string.py +++ b/py-polars/polars/expr/string.py @@ -326,12 +326,12 @@ def to_decimal( """ Convert a String column into a Decimal column. - This method infers the needed parameters `precision` and `scale`. + This method infers the needed parameter `scale`, using default precision. Parameters ---------- inference_length - Number of elements to parse to determine the `precision` and `scale`. + Number of elements to parse to determine the `scale`. Examples -------- diff --git a/py-polars/src/conversion.rs b/py-polars/src/conversion.rs index 286decb67fed..4103d7fbdf6c 100644 --- a/py-polars/src/conversion.rs +++ b/py-polars/src/conversion.rs @@ -535,7 +535,7 @@ impl FromPyObject<'_> for Wrap { "Decimal" => { let precision = ob.getattr(intern!(py, "precision"))?.extract()?; let scale = ob.getattr(intern!(py, "scale"))?.extract()?; - DataType::Decimal(precision, Some(scale)) + DataType::Decimal(precision, scale) }, "List" => { let inner = ob.getattr(intern!(py, "inner")).unwrap(); diff --git a/py-polars/src/datatypes.rs b/py-polars/src/datatypes.rs index 087159a76e66..afe3dc3e4309 100644 --- a/py-polars/src/datatypes.rs +++ b/py-polars/src/datatypes.rs @@ -31,7 +31,7 @@ pub(crate) enum PyDataType { Categorical, Struct, Binary, - Decimal(Option, usize), + Decimal(Option, Option), Array(usize), Enum(Utf8Array), } @@ -50,7 +50,7 @@ impl From<&DataType> for PyDataType { DataType::UInt64 => UInt64, DataType::Float32 => Float32, DataType::Float64 => Float64, - DataType::Decimal(p, s) => Decimal(*p, s.expect("unexpected null decimal scale")), + DataType::Decimal(p, s) => Decimal(*p, *s), DataType::Boolean => Bool, DataType::String => String, DataType::Binary => Binary, @@ -113,7 +113,7 @@ impl From for DataType { PyDataType::Categorical => Categorical(None, Default::default()), PyDataType::Enum(categories) => create_enum_data_type(categories), PyDataType::Struct => Struct(vec![]), - PyDataType::Decimal(p, s) => Decimal(p, Some(s)), + PyDataType::Decimal(p, s) => Decimal(p, s), PyDataType::Array(width) => Array(DataType::Null.into(), width), } } diff --git a/py-polars/src/series/construction.rs b/py-polars/src/series/construction.rs index 799638850a5f..189dbe3ad828 100644 --- a/py-polars/src/series/construction.rs +++ b/py-polars/src/series/construction.rs @@ -284,12 +284,19 @@ impl PySeries { } } + #[pyo3(signature = (precision, scale, name, val, strict))] #[staticmethod] - fn new_decimal(name: &str, val: Vec>>, strict: bool) -> PyResult { + fn new_decimal( + precision: Option, + scale: Option, + name: &str, + val: Vec>>, + strict: bool, + ) -> PyResult { // TODO: do we have to respect 'strict' here? It's possible if we want to. let avs = slice_extract_wrapped(&val); // Create a fake dtype with a placeholder "none" scale, to be inferred later. - let dtype = DataType::Decimal(None, None); + let dtype = DataType::Decimal(precision, scale); let s = Series::from_any_values_and_dtype(name, avs, &dtype, strict) .map_err(PyPolarsErr::from)?; Ok(s.into())