polars-df 0.25.1 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -1
  3. data/Cargo.lock +268 -95
  4. data/LICENSE.txt +1 -1
  5. data/README.md +1 -3
  6. data/ext/polars/Cargo.toml +18 -18
  7. data/ext/polars/src/catalog/unity.rs +15 -20
  8. data/ext/polars/src/conversion/any_value.rs +25 -24
  9. data/ext/polars/src/conversion/chunked_array.rs +58 -56
  10. data/ext/polars/src/conversion/datetime.rs +58 -7
  11. data/ext/polars/src/conversion/mod.rs +155 -141
  12. data/ext/polars/src/dataframe/export.rs +15 -12
  13. data/ext/polars/src/dataframe/general.rs +5 -4
  14. data/ext/polars/src/dataframe/map.rs +6 -4
  15. data/ext/polars/src/error.rs +1 -1
  16. data/ext/polars/src/expr/array.rs +0 -24
  17. data/ext/polars/src/expr/datatype.rs +3 -2
  18. data/ext/polars/src/expr/datetime.rs +4 -4
  19. data/ext/polars/src/expr/general.rs +27 -15
  20. data/ext/polars/src/expr/list.rs +0 -26
  21. data/ext/polars/src/functions/business.rs +2 -2
  22. data/ext/polars/src/functions/io.rs +4 -3
  23. data/ext/polars/src/functions/lazy.rs +58 -46
  24. data/ext/polars/src/functions/meta.rs +6 -5
  25. data/ext/polars/src/functions/mod.rs +0 -1
  26. data/ext/polars/src/functions/utils.rs +4 -2
  27. data/ext/polars/src/interop/arrow/mod.rs +4 -2
  28. data/ext/polars/src/interop/numo/to_numo_series.rs +26 -25
  29. data/ext/polars/src/io/scan_options.rs +6 -3
  30. data/ext/polars/src/io/sink_options.rs +2 -0
  31. data/ext/polars/src/lazyframe/general.rs +28 -13
  32. data/ext/polars/src/lazyframe/optflags.rs +2 -1
  33. data/ext/polars/src/lib.rs +14 -33
  34. data/ext/polars/src/map/lazy.rs +5 -2
  35. data/ext/polars/src/map/series.rs +19 -18
  36. data/ext/polars/src/on_startup.rs +16 -7
  37. data/ext/polars/src/ruby/numo.rs +3 -4
  38. data/ext/polars/src/ruby/rb_modules.rs +2 -4
  39. data/ext/polars/src/ruby/ruby_udf.rs +7 -9
  40. data/ext/polars/src/ruby/utils.rs +12 -1
  41. data/ext/polars/src/series/aggregation.rs +13 -1
  42. data/ext/polars/src/series/export.rs +33 -38
  43. data/ext/polars/src/series/general.rs +4 -3
  44. data/ext/polars/src/series/map.rs +3 -2
  45. data/ext/polars/src/series/scatter.rs +4 -4
  46. data/ext/polars/src/utils.rs +31 -7
  47. data/lib/polars/array_expr.rb +23 -7
  48. data/lib/polars/array_name_space.rb +16 -2
  49. data/lib/polars/binary_name_space.rb +32 -0
  50. data/lib/polars/data_frame.rb +73 -10
  51. data/lib/polars/date_time_expr.rb +91 -3
  52. data/lib/polars/date_time_name_space.rb +7 -1
  53. data/lib/polars/expr.rb +122 -44
  54. data/lib/polars/functions/business.rb +2 -2
  55. data/lib/polars/functions/eager.rb +80 -7
  56. data/lib/polars/functions/lazy.rb +5 -2
  57. data/lib/polars/io/csv.rb +27 -5
  58. data/lib/polars/io/ipc.rb +1 -1
  59. data/lib/polars/io/lines.rb +4 -4
  60. data/lib/polars/io/sink_options.rb +4 -2
  61. data/lib/polars/lazy_frame.rb +97 -14
  62. data/lib/polars/list_expr.rb +21 -7
  63. data/lib/polars/list_name_space.rb +16 -2
  64. data/lib/polars/query_opt_flags.rb +22 -5
  65. data/lib/polars/selectors.rb +1 -1
  66. data/lib/polars/series.rb +88 -19
  67. data/lib/polars/sql_context.rb +2 -2
  68. data/lib/polars/string_cache.rb +19 -72
  69. data/lib/polars/string_expr.rb +1 -7
  70. data/lib/polars/string_name_space.rb +1 -7
  71. data/lib/polars/utils/construction/series.rb +8 -3
  72. data/lib/polars/utils/convert.rb +16 -6
  73. data/lib/polars/utils/parse.rb +7 -0
  74. data/lib/polars/utils/reduce_balanced.rb +43 -0
  75. data/lib/polars/utils/various.rb +5 -0
  76. data/lib/polars/version.rb +1 -1
  77. data/lib/polars.rb +1 -1
  78. metadata +3 -17
  79. data/ext/polars/src/functions/string_cache.rs +0 -24
data/LICENSE.txt CHANGED
@@ -1,5 +1,5 @@
1
1
  Copyright (c) 2025 Ritchie Vink
2
- Copyright (c) 2022-2025 Andrew Kane
2
+ Copyright (c) 2022-2026 Andrew Kane
3
3
  Some portions Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
data/README.md CHANGED
@@ -32,9 +32,7 @@ You can follow [Polars tutorials](https://docs.pola.rs/user-guide/getting-starte
32
32
  - [DataFrame](https://www.rubydoc.info/gems/polars-df/Polars/DataFrame)
33
33
  - [LazyFrame](https://www.rubydoc.info/gems/polars-df/Polars/LazyFrame)
34
34
 
35
- ## Examples
36
-
37
- ### Creating DataFrames
35
+ ## Creating DataFrames
38
36
 
39
37
  From a CSV
40
38
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "polars-ruby"
3
- version = "0.25.1"
3
+ version = "0.26.0"
4
4
  license = "MIT"
5
5
  authors = ["Andrew Kane <andrew@ankane.org>"]
6
6
  edition = "2024"
@@ -13,37 +13,38 @@ crate-type = ["cdylib"]
13
13
 
14
14
  [dependencies]
15
15
  ahash = "0.8"
16
- arrow = { package = "polars-arrow", version = "=0.53.0" }
16
+ arrow = { package = "polars-arrow", version = "=0.54.4" }
17
17
  bytes = "1"
18
18
  chrono = "0.4"
19
+ chrono-tz = "0.10"
19
20
  either = "1.8"
20
21
  magnus = { version = "0.8", features = ["chrono"] }
21
22
  num-traits = "0.2"
22
23
  parking_lot = "0.12"
23
- polars-buffer = "=0.53.0"
24
- polars-compute = "=0.53.0"
25
- polars-core = "=0.53.0"
26
- polars-dtype = "=0.53.0"
27
- polars-error = "=0.53.0"
28
- polars-io = "=0.53.0"
29
- polars-lazy = { version = "=0.53.0", features = ["catalog"] }
30
- polars-ops = "=0.53.0"
31
- polars-plan = "=0.53.0"
32
- polars-parquet = "=0.53.0"
33
- polars-testing = "=0.53.0"
34
- polars-utils = "=0.53.0"
24
+ polars-buffer = "=0.54.4"
25
+ polars-compute = "=0.54.4"
26
+ polars-config = "=0.54.4"
27
+ polars-core = "=0.54.4"
28
+ polars-dtype = "=0.54.4"
29
+ polars-error = "=0.54.4"
30
+ polars-io = "=0.54.4"
31
+ polars-lazy = { version = "=0.54.4", features = ["catalog"] }
32
+ polars-ops = "=0.54.4"
33
+ polars-plan = "=0.54.4"
34
+ polars-parquet = "=0.54.4"
35
+ polars-testing = "=0.54.4"
36
+ polars-utils = "=0.54.4"
35
37
  rayon = "1.9"
36
38
  rb-sys = "0.9"
37
39
  regex = "1"
38
40
  serde_json = "1"
39
41
 
40
42
  [dependencies.polars]
41
- version = "=0.53.0"
43
+ version = "=0.54.4"
42
44
  features = [
43
45
  "abs",
44
46
  "approx_unique",
45
47
  "arg_where",
46
- "array_any_all",
47
48
  "array_count",
48
49
  "array_to_struct",
49
50
  "asof_join",
@@ -93,7 +94,6 @@ features = [
93
94
  "is_unique",
94
95
  "json",
95
96
  "lazy",
96
- "list_any_all",
97
97
  "list_count",
98
98
  "list_drop_nulls",
99
99
  "list_eval",
@@ -154,7 +154,7 @@ features = [
154
154
  mimalloc = { version = "0.1", default-features = false }
155
155
 
156
156
  [target.'cfg(target_os = "linux")'.dependencies]
157
- tikv-jemallocator = { version = "0.6", features = ["disable_initial_exec_tls"] }
157
+ tikv-jemallocator = { version = "0.7", features = ["disable_initial_exec_tls"] }
158
158
 
159
159
  [features]
160
160
  default = []
@@ -1,17 +1,18 @@
1
1
  use std::str::FromStr;
2
2
 
3
3
  use magnus::value::{Lazy, ReprValue};
4
- use magnus::{IntoValue, Module, RClass, RHash, RModule, Ruby, Value};
4
+ use magnus::{Module, RClass, RHash, RModule, Ruby, Value};
5
5
  use polars::prelude::{PlHashMap, PlSmallStr, PolarsError, Schema};
6
+ use polars_core::runtime::ASYNC;
6
7
  use polars_io::catalog::unity::client::{CatalogClient, CatalogClientBuilder};
7
8
  use polars_io::catalog::unity::models::{
8
9
  CatalogInfo, ColumnInfo, DataSourceFormat, NamespaceInfo, TableInfo, TableType,
9
10
  };
10
11
  use polars_io::catalog::unity::schema::parse_type_json_str;
11
- use polars_io::pl_async;
12
12
 
13
13
  use crate::rb_modules::polars;
14
14
  use crate::ruby::gvl::GvlExt;
15
+ use crate::ruby::utils::TryIntoValue;
15
16
  use crate::utils::EnterPolarsExt;
16
17
  use crate::utils::to_rb_err;
17
18
  use crate::{RbResult, Wrap};
@@ -88,9 +89,7 @@ impl RbCatalogClient {
88
89
  }
89
90
 
90
91
  pub fn list_catalogs(rb: &Ruby, self_: &Self) -> RbResult<Value> {
91
- let v = rb.enter_polars(|| {
92
- pl_async::get_runtime().block_in_place_on(self_.client().list_catalogs())
93
- })?;
92
+ let v = rb.enter_polars(|| ASYNC.block_in_place_on(self_.client().list_catalogs()))?;
94
93
 
95
94
  let mut opt_err = None;
96
95
 
@@ -111,7 +110,7 @@ impl RbCatalogClient {
111
110
 
112
111
  pub fn list_namespaces(rb: &Ruby, self_: &Self, catalog_name: String) -> RbResult<Value> {
113
112
  let v = rb.enter_polars(|| {
114
- pl_async::get_runtime().block_in_place_on(self_.client().list_namespaces(&catalog_name))
113
+ ASYNC.block_in_place_on(self_.client().list_namespaces(&catalog_name))
115
114
  })?;
116
115
 
117
116
  let mut opt_err = None;
@@ -139,8 +138,7 @@ impl RbCatalogClient {
139
138
  namespace: String,
140
139
  ) -> RbResult<Value> {
141
140
  let v = rb.enter_polars(|| {
142
- pl_async::get_runtime()
143
- .block_in_place_on(self_.client().list_tables(&catalog_name, &namespace))
141
+ ASYNC.block_in_place_on(self_.client().list_tables(&catalog_name, &namespace))
144
142
  })?;
145
143
 
146
144
  let mut opt_err = None;
@@ -172,7 +170,7 @@ impl RbCatalogClient {
172
170
  ) -> RbResult<Value> {
173
171
  let table_info = rb
174
172
  .enter_polars(|| {
175
- pl_async::get_runtime().block_in_place_on(self_.client().get_table_info(
173
+ ASYNC.block_in_place_on(self_.client().get_table_info(
176
174
  &table_name,
177
175
  &catalog_name,
178
176
  &namespace,
@@ -192,7 +190,7 @@ impl RbCatalogClient {
192
190
  ) -> RbResult<Value> {
193
191
  let catalog_info = rb
194
192
  .detach(|| {
195
- pl_async::get_runtime().block_in_place_on(self_.client().create_catalog(
193
+ ASYNC.block_in_place_on(self_.client().create_catalog(
196
194
  &catalog_name,
197
195
  comment.as_deref(),
198
196
  storage_root.as_deref(),
@@ -209,11 +207,8 @@ impl RbCatalogClient {
209
207
  catalog_name: String,
210
208
  force: bool,
211
209
  ) -> RbResult<()> {
212
- rb.detach(|| {
213
- pl_async::get_runtime()
214
- .block_in_place_on(self_.client().delete_catalog(&catalog_name, force))
215
- })
216
- .map_err(to_rb_err)
210
+ rb.detach(|| ASYNC.block_in_place_on(self_.client().delete_catalog(&catalog_name, force)))
211
+ .map_err(to_rb_err)
217
212
  }
218
213
 
219
214
  pub fn create_namespace(
@@ -226,7 +221,7 @@ impl RbCatalogClient {
226
221
  ) -> RbResult<Value> {
227
222
  let namespace_info = rb
228
223
  .detach(|| {
229
- pl_async::get_runtime().block_in_place_on(self_.client().create_namespace(
224
+ ASYNC.block_in_place_on(self_.client().create_namespace(
230
225
  &catalog_name,
231
226
  &namespace,
232
227
  comment.as_deref(),
@@ -246,7 +241,7 @@ impl RbCatalogClient {
246
241
  force: bool,
247
242
  ) -> RbResult<()> {
248
243
  rb.detach(|| {
249
- pl_async::get_runtime().block_in_place_on(self_.client().delete_namespace(
244
+ ASYNC.block_in_place_on(self_.client().delete_namespace(
250
245
  &catalog_name,
251
246
  &namespace,
252
247
  force,
@@ -270,7 +265,7 @@ impl RbCatalogClient {
270
265
  ) -> RbResult<Value> {
271
266
  let table_info = rb
272
267
  .detach(|| {
273
- pl_async::get_runtime().block_in_place_on(
268
+ ASYNC.block_in_place_on(
274
269
  self_.client().create_table(
275
270
  &catalog_name,
276
271
  &namespace,
@@ -303,7 +298,7 @@ impl RbCatalogClient {
303
298
  table_name: String,
304
299
  ) -> RbResult<()> {
305
300
  rb.detach(|| {
306
- pl_async::get_runtime().block_in_place_on(self_.client().delete_table(
301
+ ASYNC.block_in_place_on(self_.client().delete_table(
307
302
  &catalog_name,
308
303
  &namespace,
309
304
  &table_name,
@@ -313,7 +308,7 @@ impl RbCatalogClient {
313
308
  }
314
309
 
315
310
  pub fn type_json_to_polars_type(rb: &Ruby, type_json: String) -> RbResult<Value> {
316
- Ok(Wrap(parse_type_json_str(&type_json).map_err(to_rb_err)?).into_value_with(rb))
311
+ Wrap(parse_type_json_str(&type_json).map_err(to_rb_err)?).try_into_value_with(rb)
317
312
  }
318
313
  }
319
314
 
@@ -14,10 +14,11 @@ use super::{ObjectValue, Wrap, struct_dict};
14
14
 
15
15
  use crate::rb_modules::pl_utils;
16
16
  use crate::ruby::exceptions::RbOverflowError;
17
+ use crate::ruby::utils::TryIntoValue;
17
18
  use crate::{RbErr, RbPolarsErr, RbResult, RbSeries, RbValueError};
18
19
 
19
- impl IntoValue for Wrap<AnyValue<'_>> {
20
- fn into_value_with(self, ruby: &Ruby) -> Value {
20
+ impl TryIntoValue for Wrap<AnyValue<'_>> {
21
+ fn try_into_value_with(self, ruby: &Ruby) -> RbResult<Value> {
21
22
  any_value_into_rb_object(self.0, ruby)
22
23
  }
23
24
  }
@@ -28,8 +29,8 @@ impl TryConvert for Wrap<AnyValue<'_>> {
28
29
  }
29
30
  }
30
31
 
31
- pub(crate) fn any_value_into_rb_object(av: AnyValue, ruby: &Ruby) -> Value {
32
- match av {
32
+ pub(crate) fn any_value_into_rb_object(av: AnyValue, ruby: &Ruby) -> RbResult<Value> {
33
+ let rb_object = match av {
33
34
  AnyValue::UInt8(v) => ruby.into_value(v),
34
35
  AnyValue::UInt16(v) => ruby.into_value(v),
35
36
  AnyValue::UInt32(v) => ruby.into_value(v),
@@ -53,41 +54,38 @@ pub(crate) fn any_value_into_rb_object(av: AnyValue, ruby: &Ruby) -> Value {
53
54
  AnyValue::CategoricalOwned(cat, map) | AnyValue::EnumOwned(cat, map) => unsafe {
54
55
  map.cat_to_str_unchecked(cat).into_value_with(ruby)
55
56
  },
56
- AnyValue::Date(v) => pl_utils(ruby).funcall("_to_ruby_date", (v,)).unwrap(),
57
+ AnyValue::Date(v) => pl_utils(ruby).funcall("_to_ruby_date", (v,))?,
57
58
  AnyValue::Datetime(v, time_unit, time_zone) => {
58
- datetime_to_rb_object(v, time_unit, time_zone)
59
+ datetime_to_rb_object(ruby, v, time_unit, time_zone)?
59
60
  }
60
61
  AnyValue::DatetimeOwned(v, time_unit, time_zone) => {
61
- datetime_to_rb_object(v, time_unit, time_zone.as_ref().map(AsRef::as_ref))
62
+ datetime_to_rb_object(ruby, v, time_unit, time_zone.as_ref().map(AsRef::as_ref))?
62
63
  }
63
64
  AnyValue::Duration(v, time_unit) => {
64
65
  let time_unit = time_unit.to_ascii();
65
- pl_utils(ruby)
66
- .funcall("_to_ruby_duration", (v, time_unit))
67
- .unwrap()
66
+ pl_utils(ruby).funcall("_to_ruby_duration", (v, time_unit))?
68
67
  }
69
- AnyValue::Time(v) => pl_utils(ruby).funcall("_to_ruby_time", (v,)).unwrap(),
70
- AnyValue::Array(v, _) | AnyValue::List(v) => RbSeries::new(v).to_a().unwrap().as_value(),
71
- ref av @ AnyValue::Struct(_, _, flds) => struct_dict(ruby, av._iter_struct_av(), flds),
72
- AnyValue::StructOwned(payload) => struct_dict(ruby, payload.0.into_iter(), &payload.1),
68
+ AnyValue::Time(v) => pl_utils(ruby).funcall("_to_ruby_time", (v,))?,
69
+ AnyValue::Array(v, _) | AnyValue::List(v) => RbSeries::to_a(ruby, &RbSeries::new(v))?,
70
+ ref av @ AnyValue::Struct(_, _, flds) => struct_dict(ruby, av._iter_struct_av(), flds)?,
71
+ AnyValue::StructOwned(payload) => struct_dict(ruby, payload.0.into_iter(), &payload.1)?,
73
72
  AnyValue::Object(v) => {
74
73
  let object = v.as_any().downcast_ref::<ObjectValue>().unwrap();
75
- object.to_value()
74
+ object.clone().into_value_with(ruby)
76
75
  }
77
76
  AnyValue::ObjectOwned(v) => {
78
77
  let object = v.0.as_any().downcast_ref::<ObjectValue>().unwrap();
79
- object.to_value()
78
+ object.clone().into_value_with(ruby)
80
79
  }
81
80
  AnyValue::Binary(v) => ruby.str_from_slice(v).as_value(),
82
81
  AnyValue::BinaryOwned(v) => ruby.str_from_slice(&v).as_value(),
83
82
  AnyValue::Decimal(v, prec, scale) => {
84
83
  let mut buf = DecimalFmtBuffer::new();
85
84
  let s = buf.format_dec128(v, scale, false, false);
86
- pl_utils(ruby)
87
- .funcall("_to_ruby_decimal", (prec, s))
88
- .unwrap()
85
+ pl_utils(ruby).funcall("_to_ruby_decimal", (prec, s))?
89
86
  }
90
- }
87
+ };
88
+ Ok(rb_object)
91
89
  }
92
90
 
93
91
  pub(crate) fn rb_object_to_any_value<'s>(
@@ -130,7 +128,7 @@ pub(crate) fn rb_object_to_any_value<'s>(
130
128
 
131
129
  fn get_str(ob: Value, _strict: bool) -> RbResult<AnyValue<'static>> {
132
130
  let ruby = Ruby::get_with(ob);
133
- let v = RString::from_value(ob).unwrap();
131
+ let v = RString::try_convert(ob)?;
134
132
  if v.enc_get() == ruby.utf8_encindex() {
135
133
  Ok(AnyValue::StringOwned(v.to_string()?.into()))
136
134
  } else {
@@ -139,7 +137,7 @@ pub(crate) fn rb_object_to_any_value<'s>(
139
137
  }
140
138
 
141
139
  fn get_list(ob: Value, _strict: bool) -> RbResult<AnyValue<'static>> {
142
- let v = RArray::from_value(ob).unwrap();
140
+ let v = RArray::try_convert(ob)?;
143
141
  if v.is_empty() {
144
142
  Ok(AnyValue::List(Series::new_empty(
145
143
  PlSmallStr::EMPTY,
@@ -242,7 +240,7 @@ pub(crate) fn rb_object_to_any_value<'s>(
242
240
  }
243
241
  }
244
242
 
245
- let (sign, digits, _, exp): (i8, String, i32, i32) = ob.funcall("split", ()).unwrap();
243
+ let (sign, digits, _, exp): (i8, String, i32, i32) = ob.funcall("split", ())?;
246
244
  let (mut v, scale) = abs_decimal_from_digits(digits, exp).ok_or_else(|| {
247
245
  RbErr::from(RbPolarsErr::Other(
248
246
  "BigDecimal is too large to fit in Decimal128".into(),
@@ -279,7 +277,10 @@ pub(crate) fn rb_object_to_any_value<'s>(
279
277
  get_datetime(ob, strict)
280
278
  } else if ob.is_kind_of(crate::ruby::rb_modules::date(&ruby)) {
281
279
  get_date(ob, strict)
282
- } else if ob.is_kind_of(crate::ruby::rb_modules::bigdecimal(&ruby)) {
280
+ } else if crate::ruby::rb_modules::bigdecimal(&ruby)
281
+ .map(|cls| ob.is_kind_of(cls))
282
+ .unwrap_or(false)
283
+ {
283
284
  get_decimal(ob, strict)
284
285
  } else {
285
286
  if allow_object {
@@ -1,11 +1,13 @@
1
- use magnus::{IntoValue, RString, Ruby, TryConvert, Value, prelude::*};
1
+ use magnus::{IntoValue, RArray, RString, Ruby, TryConvert, Value, prelude::*};
2
2
  use polars::prelude::*;
3
3
  use polars_compute::decimal::DecimalFmtBuffer;
4
4
 
5
+ use super::datetime::datetime_to_rb_object;
5
6
  use super::{Wrap, get_rbseq, struct_dict};
6
7
 
7
8
  use crate::RbResult;
8
9
  use crate::rb_modules::pl_utils;
10
+ use crate::ruby::utils::TryIntoValue;
9
11
 
10
12
  impl TryConvert for Wrap<StringChunked> {
11
13
  fn try_convert(obj: Value) -> RbResult<Self> {
@@ -41,7 +43,7 @@ impl TryConvert for Wrap<BinaryChunked> {
41
43
 
42
44
  impl IntoValue for Wrap<&StringChunked> {
43
45
  fn into_value_with(self, ruby: &Ruby) -> Value {
44
- let iter = self.0.into_iter();
46
+ let iter = self.0.iter();
45
47
  ruby.ary_from_iter(iter).as_value()
46
48
  }
47
49
  }
@@ -50,96 +52,96 @@ impl IntoValue for Wrap<&BinaryChunked> {
50
52
  fn into_value_with(self, ruby: &Ruby) -> Value {
51
53
  let iter = self
52
54
  .0
53
- .into_iter()
55
+ .iter()
54
56
  .map(|opt_bytes| opt_bytes.map(|v| ruby.str_from_slice(v)));
55
57
  ruby.ary_from_iter(iter).as_value()
56
58
  }
57
59
  }
58
60
 
59
- impl IntoValue for Wrap<&StructChunked> {
60
- fn into_value_with(self, ruby: &Ruby) -> Value {
61
+ impl TryIntoValue for Wrap<&StructChunked> {
62
+ fn try_into_value_with(self, ruby: &Ruby) -> RbResult<Value> {
61
63
  let s = self.0.clone().into_series();
62
64
  // todo! iterate its chunks and flatten.
63
65
  // make series::iter() accept a chunk index.
64
66
  let s = s.rechunk();
65
67
  let iter = s.iter().map(|av| match av {
66
68
  AnyValue::Struct(_, _, flds) => struct_dict(ruby, av._iter_struct_av(), flds),
67
- AnyValue::Null => ruby.qnil().as_value(),
69
+ AnyValue::Null => Ok(ruby.qnil().as_value()),
68
70
  _ => unreachable!(),
69
71
  });
70
72
 
71
- ruby.ary_from_iter(iter).as_value()
73
+ ruby.ary_try_from_iter(iter).map(|v| v.as_value())
72
74
  }
73
75
  }
74
76
 
75
- impl IntoValue for Wrap<&DurationChunked> {
76
- fn into_value_with(self, ruby: &Ruby) -> Value {
77
+ impl TryIntoValue for Wrap<&DurationChunked> {
78
+ fn try_into_value_with(self, ruby: &Ruby) -> RbResult<Value> {
77
79
  let utils = pl_utils(ruby);
78
80
  let time_unit = Wrap(self.0.time_unit()).into_value_with(ruby);
79
- let iter = self.0.physical().into_iter().map(|opt_v| {
80
- opt_v.map(|v| {
81
- utils
82
- .funcall::<_, _, Value>("_to_ruby_duration", (v, time_unit))
83
- .unwrap()
84
- })
81
+ let iter = self.0.physical().iter().map(|opt_v| {
82
+ opt_v
83
+ .map(|v| utils.funcall::<_, _, Value>("_to_ruby_duration", (v, time_unit)))
84
+ .transpose()
85
85
  });
86
- ruby.ary_from_iter(iter).as_value()
86
+ ruby.ary_try_from_iter(iter).map(|v| v.as_value())
87
87
  }
88
88
  }
89
89
 
90
- impl IntoValue for Wrap<&DatetimeChunked> {
91
- fn into_value_with(self, ruby: &Ruby) -> Value {
92
- let utils = pl_utils(ruby);
93
- let time_unit = Wrap(self.0.time_unit()).into_value_with(ruby);
94
- let time_zone = self
95
- .0
96
- .time_zone()
97
- .as_deref()
98
- .map(|v| v.into_value_with(ruby));
99
- let iter = self.0.physical().into_iter().map(|opt_v| {
100
- opt_v.map(|v| {
101
- utils
102
- .funcall::<_, _, Value>("_to_ruby_datetime", (v, time_unit, time_zone))
103
- .unwrap()
104
- })
90
+ impl TryIntoValue for Wrap<&DatetimeChunked> {
91
+ fn try_into_value_with(self, ruby: &Ruby) -> RbResult<Value> {
92
+ let time_zone = self.0.time_zone().as_ref();
93
+ let time_unit = self.0.time_unit();
94
+ let iter = self.0.physical().iter().map(|opt_v| {
95
+ opt_v
96
+ .map(|v| datetime_to_rb_object(ruby, v, time_unit, time_zone))
97
+ .transpose()
105
98
  });
106
- ruby.ary_from_iter(iter).as_value()
99
+ ruby.ary_try_from_iter(iter).map(|v| v.as_value())
107
100
  }
108
101
  }
109
102
 
110
- impl IntoValue for Wrap<&TimeChunked> {
111
- fn into_value_with(self, ruby: &Ruby) -> Value {
103
+ impl TryIntoValue for Wrap<&TimeChunked> {
104
+ fn try_into_value_with(self, ruby: &Ruby) -> RbResult<Value> {
112
105
  let utils = pl_utils(ruby);
113
- let iter = self.0.physical().into_iter().map(|opt_v| {
114
- opt_v.map(|v| utils.funcall::<_, _, Value>("_to_ruby_time", (v,)).unwrap())
106
+ let iter = self.0.physical().iter().map(|opt_v| {
107
+ opt_v
108
+ .map(|v| utils.funcall::<_, _, Value>("_to_ruby_time", (v,)))
109
+ .transpose()
115
110
  });
116
- ruby.ary_from_iter(iter).as_value()
111
+ ruby.ary_try_from_iter(iter).map(|v| v.as_value())
117
112
  }
118
113
  }
119
114
 
120
- impl IntoValue for Wrap<&DateChunked> {
121
- fn into_value_with(self, ruby: &Ruby) -> Value {
115
+ impl TryIntoValue for Wrap<&DateChunked> {
116
+ fn try_into_value_with(self, ruby: &Ruby) -> RbResult<Value> {
122
117
  let utils = pl_utils(ruby);
123
- let iter = self.0.physical().into_iter().map(|opt_v| {
124
- opt_v.map(|v| utils.funcall::<_, _, Value>("_to_ruby_date", (v,)).unwrap())
118
+ let iter = self.0.physical().iter().map(|opt_v| {
119
+ opt_v
120
+ .map(|v| utils.funcall::<_, _, Value>("_to_ruby_date", (v,)))
121
+ .transpose()
125
122
  });
126
- ruby.ary_from_iter(iter).as_value()
123
+ ruby.ary_try_from_iter(iter).map(|v| v.as_value())
127
124
  }
128
125
  }
129
126
 
130
- impl IntoValue for Wrap<&DecimalChunked> {
131
- fn into_value_with(self, ruby: &Ruby) -> Value {
132
- let utils = pl_utils(ruby);
133
- let rb_precision = self.0.precision().into_value_with(ruby);
134
- let mut buf = DecimalFmtBuffer::new();
135
- let iter = self.0.physical().into_iter().map(|opt_v| {
136
- opt_v.map(|v| {
137
- let s = buf.format_dec128(v, self.0.scale(), false, false);
138
- utils
139
- .funcall::<_, _, Value>("_to_ruby_decimal", (rb_precision, s))
140
- .unwrap()
141
- })
142
- });
143
- ruby.ary_from_iter(iter).as_value()
127
+ impl TryIntoValue for Wrap<&DecimalChunked> {
128
+ fn try_into_value_with(self, ruby: &Ruby) -> RbResult<Value> {
129
+ let iter = decimal_to_rbobject_iter(ruby, self.0)?;
130
+ Ok(iter.as_value())
144
131
  }
145
132
  }
133
+
134
+ pub(crate) fn decimal_to_rbobject_iter(ruby: &Ruby, ca: &DecimalChunked) -> RbResult<RArray> {
135
+ let utils = pl_utils(ruby);
136
+ let rb_precision = ca.precision().into_value_with(ruby);
137
+ let mut buf = DecimalFmtBuffer::new();
138
+ let iter = ca.physical().iter().map(move |opt_v| {
139
+ opt_v
140
+ .map(|v| {
141
+ let s = buf.format_dec128(v, ca.scale(), false, false);
142
+ utils.funcall::<_, _, Value>("_to_ruby_decimal", (rb_precision, s))
143
+ })
144
+ .transpose()
145
+ });
146
+ ruby.ary_try_from_iter(iter)
147
+ }
@@ -1,12 +1,63 @@
1
- use magnus::{Ruby, Value, prelude::*};
1
+ //! Utilities for converting dates, times, datetimes, and so on.
2
+
3
+ use std::str::FromStr;
4
+
5
+ use chrono::{DateTime, Datelike, FixedOffset, NaiveDateTime, TimeDelta, TimeZone as _};
6
+ use chrono_tz::Tz;
7
+ use magnus::{IntoValue, Ruby, Value, prelude::*};
2
8
  use polars::prelude::*;
3
9
 
4
10
  use crate::rb_modules::pl_utils;
11
+ use crate::{RbPolarsErr, RbResult};
12
+
13
+ pub fn elapsed_offset_to_timedelta(elapsed: i64, time_unit: TimeUnit) -> TimeDelta {
14
+ let (in_second, nano_multiplier) = match time_unit {
15
+ TimeUnit::Nanoseconds => (1_000_000_000, 1),
16
+ TimeUnit::Microseconds => (1_000_000, 1_000),
17
+ TimeUnit::Milliseconds => (1_000, 1_000_000),
18
+ };
19
+ let mut elapsed_sec = elapsed / in_second;
20
+ let mut elapsed_nanos = nano_multiplier * (elapsed % in_second);
21
+ if elapsed_nanos < 0 {
22
+ // TimeDelta expects nanos to always be positive.
23
+ elapsed_sec -= 1;
24
+ elapsed_nanos += 1_000_000_000;
25
+ }
26
+ TimeDelta::new(elapsed_sec, elapsed_nanos as u32).unwrap()
27
+ }
28
+
29
+ /// Convert time-units-since-epoch to a more structured object.
30
+ pub fn timestamp_to_naive_datetime(since_epoch: i64, time_unit: TimeUnit) -> NaiveDateTime {
31
+ DateTime::UNIX_EPOCH.naive_utc() + elapsed_offset_to_timedelta(since_epoch, time_unit)
32
+ }
5
33
 
6
- pub fn datetime_to_rb_object(v: i64, tu: TimeUnit, tz: Option<&TimeZone>) -> Value {
7
- let ruby = Ruby::get().unwrap();
8
- let tu = tu.to_ascii();
9
- pl_utils(&ruby)
10
- .funcall("_to_ruby_datetime", (v, tu, tz.map(|v| v.to_string())))
11
- .unwrap()
34
+ pub fn datetime_to_rb_object(
35
+ ruby: &Ruby,
36
+ v: i64,
37
+ tu: TimeUnit,
38
+ tz: Option<&TimeZone>,
39
+ ) -> RbResult<Value> {
40
+ if let Some(time_zone) = tz {
41
+ if let Ok(tz) = Tz::from_str(time_zone) {
42
+ let utc_datetime = DateTime::UNIX_EPOCH + elapsed_offset_to_timedelta(v, tu);
43
+ if utc_datetime.year() >= 2100 {
44
+ // chrono-tz does not support dates after 2100
45
+ // https://github.com/chronotope/chrono-tz/issues/135
46
+ pl_utils(ruby).funcall("_to_ruby_datetime", (v, tu.to_ascii(), time_zone.as_str()))
47
+ } else {
48
+ let datetime = utc_datetime.with_timezone(&tz);
49
+ Ok(datetime.fixed_offset().into_value_with(ruby))
50
+ }
51
+ } else if let Ok(tz) = FixedOffset::from_str(time_zone) {
52
+ let naive_datetime = timestamp_to_naive_datetime(v, tu);
53
+ let datetime = tz.from_utc_datetime(&naive_datetime);
54
+ Ok(datetime.into_value_with(ruby))
55
+ } else {
56
+ Err(RbPolarsErr::Other(format!("Could not parse timezone: {time_zone}")).into())
57
+ }
58
+ } else {
59
+ Ok(timestamp_to_naive_datetime(v, tu)
60
+ .and_utc()
61
+ .into_value_with(ruby))
62
+ }
12
63
  }