Skip to content

Commit 656cdb3

Browse files
feat(vector) Add automatic type inference from items during init
1 parent c4999a9 commit 656cdb3

3 files changed

Lines changed: 141 additions & 76 deletions

File tree

docs/docs/content/documentation/data-types/vector.md

Lines changed: 64 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,47 +5,45 @@ The `Vector` type represents a collection of elements of a specific type. All el
55

66
## Usage
77

8+
The type is automatically inferred from the first item. You can also specify `ray_type` explicitly to cast items to a specific type.
9+
810
```python
9-
>>> from rayforce import Vector, I64, Symbol
11+
>>> from rayforce import Vector, I64, F64, Symbol
1012

11-
>>> int_vector = Vector(ray_type=I64, length=3)
13+
# Type is inferred from items
14+
>>> int_vector = Vector([1, 2, 3])
1215
>>> int_vector
13-
Vector(5) # 5 represents a type code of I64
16+
[I64(1), I64(2), I64(3)]
17+
18+
>>> float_vector = Vector([1.5, 2.5, 3.5])
19+
>>> float_vector
20+
[F64(1.5), F64(2.5), F64(3.5)]
1421

15-
>>> symbol_vector = Vector(ray_type=Symbol, items=["apple", "banana", "cherry"])
22+
>>> symbol_vector = Vector(["apple", "banana", "cherry"])
1623
>>> symbol_vector
17-
Vector(5) # 6 represents a type code of Symbol
18-
19-
>>> timestamp_vector = Vector(
20-
ray_type=Timestamp,
21-
items=[
22-
"2025-05-10T14:30:45+00:00",
23-
"2025-05-10T14:30:45+00:00",
24-
"2025-05-10T14:30:45+00:00",
25-
]
26-
)
27-
```
24+
[Symbol('apple'), Symbol('banana'), Symbol('cherry')]
2825

29-
### Accessing the values
30-
```python
31-
>>> [i for i in int_vector]
32-
[I64(0), I64(0), I64(0), I64(0), I64(0)]
26+
# Explicit ray_type for casting
27+
>>> Vector([1, 2, 3], ray_type=F64)
28+
[F64(1.0), F64(2.0), F64(3.0)]
3329

34-
>>> [i for i in symbol_vector]
35-
[Symbol('apple'), Symbol('banana'), Symbol('cherry')]
30+
# Pre-allocate empty vector with specific type and length
31+
>>> Vector(ray_type=I64, length=3)
32+
[I64(0), I64(0), I64(0)]
3633
```
3734

38-
### Setting the values
35+
### Accessing and Setting Values
3936
```python
40-
>>> int_vector[0] = 999
41-
[I64(999), I64(0), I64(0), I64(0), I64(0)]
42-
>>> int_vector
37+
>>> v = Vector([10, 20, 30])
38+
>>> v[0]
39+
I64(10)
4340

44-
>>> symbol_vector[0] = "pineapple"
45-
>>> [i for i in symbol_vector]
46-
[Symbol('pineapple'), Symbol('banana'), Symbol('cherry')]
41+
>>> v[0] = 999
42+
>>> v
43+
[I64(999), I64(20), I64(30)]
4744

48-
# TODO: Add push object to vector outside of it's length
45+
>>> [i for i in v]
46+
[I64(999), I64(20), I64(30)]
4947
```
5048

5149
## Operations
@@ -55,55 +53,55 @@ Vectors support a wide range of operations through mixins.
5553
### Arithmetic Operations
5654

5755
```python
58-
>>> v1 = Vector(ray_type=I64, items=[1, 2, 3])
59-
>>> v2 = Vector(ray_type=I64, items=[4, 5, 6])
56+
>>> v1 = Vector([1, 2, 3])
57+
>>> v2 = Vector([4, 5, 6])
6058

6159
>>> v1 + v2
62-
Vector([I64(5), I64(7), I64(9)])
60+
[I64(5), I64(7), I64(9)]
6361

6462
>>> v1 * 2
65-
Vector([I64(2), I64(4), I64(6)])
63+
[I64(2), I64(4), I64(6)]
6664

6765
>>> v1 - v2
68-
Vector([I64(-3), I64(-3), I64(-3)])
66+
[I64(-3), I64(-3), I64(-3)]
6967
```
7068

7169
### Comparison Operations
7270

7371
```python
74-
>>> v1 = Vector(ray_type=I64, items=[1, 5, 10])
75-
>>> v2 = Vector(ray_type=I64, items=[2, 5, 8])
72+
>>> v1 = Vector([1, 5, 10])
73+
>>> v2 = Vector([2, 5, 8])
7674

7775
>>> v1 < v2
78-
Vector([B8(True), B8(False), B8(False)])
76+
[B8(True), B8(False), B8(False)]
7977

8078
>>> v1 >= v2
81-
Vector([B8(False), B8(True), B8(True)])
79+
[B8(False), B8(True), B8(True)]
8280

8381
>>> v1.eq(v2)
84-
Vector([B8(False), B8(True), B8(False)])
82+
[B8(False), B8(True), B8(False)]
8583
```
8684

8785
### Logical Operations
8886

8987
```python
90-
>>> v1 = Vector(ray_type=B8, items=[True, False, True])
91-
>>> v2 = Vector(ray_type=B8, items=[True, True, False])
88+
>>> v1 = Vector([True, False, True])
89+
>>> v2 = Vector([True, True, False])
9290

9391
>>> v1.and_(v2)
94-
Vector([B8(True), B8(False), B8(False)])
92+
[B8(True), B8(False), B8(False)]
9593

9694
>>> v1.or_(v2)
97-
Vector([B8(True), B8(True), B8(True)])
95+
[B8(True), B8(True), B8(True)]
9896

9997
>>> v1.not_()
100-
Vector([B8(False), B8(True), B8(False)])
98+
[B8(False), B8(True), B8(False)]
10199
```
102100

103101
### Aggregation Operations
104102

105103
```python
106-
>>> v = Vector(ray_type=I64, items=[1, 2, 3, 4, 5])
104+
>>> v = Vector([1, 2, 3, 4, 5])
107105

108106
>>> v.sum()
109107
I64(15)
@@ -121,7 +119,7 @@ F64(3.0)
121119
### Element Access
122120

123121
```python
124-
>>> v = Vector(ray_type=I64, items=[10, 20, 30, 40, 50])
122+
>>> v = Vector([10, 20, 30, 40, 50])
125123

126124
>>> v.first()
127125
I64(10)
@@ -130,7 +128,7 @@ I64(10)
130128
I64(50)
131129

132130
>>> v.take(3)
133-
Vector([I64(10), I64(20), I64(30)])
131+
[I64(10), I64(20), I64(30)]
134132

135133
>>> v.at(2)
136134
I64(30)
@@ -139,65 +137,65 @@ I64(30)
139137
### Set Operations
140138

141139
```python
142-
>>> v1 = Vector(ray_type=I64, items=[1, 2, 3, 4])
143-
>>> v2 = Vector(ray_type=I64, items=[3, 4, 5, 6])
140+
>>> v1 = Vector([1, 2, 3, 4])
141+
>>> v2 = Vector([3, 4, 5, 6])
144142

145143
>>> v1.union(v2)
146-
Vector([I64(1), I64(2), I64(3), I64(4), I64(5), I64(6)])
144+
[I64(1), I64(2), I64(3), I64(4), I64(5), I64(6)]
147145

148146
>>> v1.sect(v2)
149-
Vector([I64(3), I64(4)])
147+
[I64(3), I64(4)]
150148

151149
>>> v1.except_(v2)
152-
Vector([I64(1), I64(2)])
150+
[I64(1), I64(2)]
153151
```
154152

155153
### Search Operations
156154

157155
```python
158-
>>> v = Vector(ray_type=I64, items=[10, 20, 30, 40])
156+
>>> v = Vector([10, 20, 30, 40])
159157

160158
>>> v.find(30)
161159
I64(2)
162160

163-
>>> v.within(Vector(ray_type=I64, items=[15, 35]))
164-
Vector([B8(False), B8(True), B8(True), B8(False)])
161+
>>> v.within(Vector([15, 35]))
162+
[B8(False), B8(True), B8(True), B8(False)]
165163

166-
>>> mask = Vector(ray_type=B8, items=[True, False, True, False])
164+
>>> mask = Vector([True, False, True, False])
167165
>>> v.filter(mask)
168-
Vector([I64(10), I64(30)])
166+
[I64(10), I64(30)]
169167
```
170168

171169
### Sort Operations
172170

173171
```python
174-
>>> v = Vector(ray_type=I64, items=[3, 1, 4, 1, 5])
172+
>>> v = Vector([3, 1, 4, 1, 5])
175173

176174
>>> v.asc()
177-
Vector([I64(1), I64(1), I64(3), I64(4), I64(5)])
175+
[I64(1), I64(1), I64(3), I64(4), I64(5)]
178176

179177
>>> v.desc()
180-
Vector([I64(5), I64(4), I64(3), I64(1), I64(1)])
178+
[I64(5), I64(4), I64(3), I64(1), I64(1)]
181179

182180
>>> v.iasc() # indices for ascending sort
183-
Vector([I64(1), I64(3), I64(0), I64(2), I64(4)])
181+
[I64(1), I64(3), I64(0), I64(2), I64(4)]
184182

185183
>>> v.rank()
186-
Vector([I64(2), I64(0), I64(3), I64(1), I64(4)])
184+
[I64(2), I64(0), I64(3), I64(1), I64(4)]
187185

188186
>>> v.reverse()
189-
Vector([I64(5), I64(1), I64(4), I64(1), I64(3)])
187+
[I64(5), I64(1), I64(4), I64(1), I64(3)]
190188

191189
>>> v.negate()
192-
Vector([I64(-3), I64(-1), I64(-4), I64(-1), I64(-5)])
190+
[I64(-3), I64(-1), I64(-4), I64(-1), I64(-5)]
193191
```
194192

195193
### Functional Operations
196194

197195
```python
198196
>>> from rayforce import Operation
199197

200-
>>> v = Vector(ray_type=I64, items=[1, 2, 3])
198+
>>> v = Vector([1, 2, 3])
201199
>>> v.map(Operation.NEGATE)
202-
Vector([I64(-1), I64(-2), I64(-3)])
200+
[I64(-1), I64(-2), I64(-3)]
203201
```

rayforce/types/containers/vector.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,26 +46,23 @@ def __init__(
4646

4747
elif items is not None:
4848
if ray_type is None:
49-
raise errors.RayforceInitError("ray_type required when creating Vector from value")
50-
self.ptr = self._create_from_value(items, ray_type)
49+
if not items:
50+
raise errors.RayforceInitError("Cannot infer vector type for empty items")
51+
ray_type = FFI.get_obj_type(utils.python_to_ray(items[0]))
52+
self.ptr = self._create_from_value(value=items, ray_type=ray_type)
5153

5254
elif length is not None and ray_type is not None:
5355
type_code = abs(ray_type if isinstance(ray_type, int) else ray_type.type_code)
5456
self.ptr = FFI.init_vector(type_code, length)
5557

5658
else:
5759
raise errors.RayforceInitError(
58-
"Vector requires either value, ptr, or (ray_type + length)",
60+
"Vector requires either items, ptr, or (ray_type + length)",
5961
)
6062

61-
def _create_from_value(
62-
self,
63-
value: t.Sequence[t.Any],
64-
ray_type: type[RayObject] | int | None = None,
63+
def _create_from_value( # type: ignore[override]
64+
self, value: t.Sequence[t.Any], ray_type: type[RayObject] | int
6565
) -> r.RayObject:
66-
if ray_type is None:
67-
raise errors.RayforceInitError("Element ray_type must be specified for Vector")
68-
6966
type_code = abs(ray_type if isinstance(ray_type, int) else ray_type.type_code)
7067
return FFI.init_vector(type_code, list(value))
7168

tests/types/containers/test_vector.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import datetime as dt
2+
import uuid
3+
4+
import pytest
5+
6+
from rayforce import errors
17
from rayforce import types as t
28

39

@@ -17,3 +23,67 @@ def test_vector():
1723
assert v2[0].value == 100
1824
assert v2[1].value == 200
1925
assert v2[2].value == 300
26+
27+
28+
class TestVectorTypeInference:
29+
def test_infer_int_to_i64(self):
30+
v = t.Vector([1, 2, 3])
31+
assert len(v) == 3
32+
assert isinstance(v[0], t.I64)
33+
assert v[0].value == 1
34+
35+
def test_infer_float_to_f64(self):
36+
v = t.Vector([1.5, 2.5, 3.5])
37+
assert len(v) == 3
38+
assert isinstance(v[0], t.F64)
39+
assert v[0].value == 1.5
40+
41+
def test_infer_bool_to_b8(self):
42+
v = t.Vector([True, False, True])
43+
assert len(v) == 3
44+
assert isinstance(v[0], t.B8)
45+
assert v[0].value is True
46+
47+
def test_infer_str_to_symbol(self):
48+
v = t.Vector(["apple", "banana", "cherry"])
49+
assert len(v) == 3
50+
assert isinstance(v[0], t.Symbol)
51+
assert v[0].value == "apple"
52+
53+
def test_infer_datetime_to_timestamp(self):
54+
dates = [dt.datetime(2025, 1, 1), dt.datetime(2025, 1, 2)]
55+
v = t.Vector(dates)
56+
assert len(v) == 2
57+
assert isinstance(v[0], t.Timestamp)
58+
59+
def test_infer_date_to_date(self):
60+
dates = [dt.date(2025, 1, 1), dt.date(2025, 1, 2)]
61+
v = t.Vector(dates)
62+
assert len(v) == 2
63+
assert isinstance(v[0], t.Date)
64+
65+
def test_infer_time_to_time(self):
66+
times = [dt.time(12, 30), dt.time(14, 45)]
67+
v = t.Vector(times)
68+
assert len(v) == 2
69+
assert isinstance(v[0], t.Time)
70+
71+
def test_infer_uuid_to_guid(self):
72+
uuids = [uuid.uuid4(), uuid.uuid4()]
73+
v = t.Vector(uuids)
74+
assert len(v) == 2
75+
assert isinstance(v[0], t.GUID)
76+
77+
def test_explicit_ray_type_overrides_inference(self):
78+
v = t.Vector([1, 2, 3], ray_type=t.F64)
79+
assert len(v) == 3
80+
assert isinstance(v[0], t.F64)
81+
assert v[0].value == 1.0
82+
83+
def test_empty_items_raises_error(self):
84+
with pytest.raises(errors.RayforceInitError, match="Cannot infer vector"):
85+
t.Vector([])
86+
87+
def test_none_items_creates_null_vector(self):
88+
v = t.Vector([None, None, None])
89+
assert len(v) == 3

0 commit comments

Comments
 (0)