Skip to content

Commit 878cc3b

Browse files
Merge pull request #943 from Furyzer0/master
[WIP] implement str.translate and str.maketrans
2 parents 41f4cdc + 04c017d commit 878cc3b

File tree

3 files changed

+132
-4
lines changed

3 files changed

+132
-4
lines changed

tests/snippets/strings.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,13 @@
184184
assert 'z' >= 'b'
185185
assert 'a' >= 'a'
186186

187+
# str.translate
188+
assert "abc".translate({97: '🎅', 98: None, 99: "xd"}) == "🎅xd"
189+
190+
# str.maketrans
191+
assert str.maketrans({"a": "abc", "b": None, "c": 33}) == {97: "abc", 98: None, 99: 33}
192+
assert str.maketrans("hello", "world", "rust") == {104: 119, 101: 111, 108: 108, 111: 100, 114: None, 117: None, 115: None, 116: None}
193+
187194
def try_mutate_str():
188195
word = "word"
189196
word[0] = 'x'

vm/src/obj/objstr.rs

Lines changed: 124 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ use unicode_xid::UnicodeXID;
1414
use crate::format::{FormatParseError, FormatPart, FormatString};
1515
use crate::function::{OptionalArg, PyFuncArgs};
1616
use crate::pyobject::{
17-
IdProtocol, IntoPyObject, PyClassImpl, PyContext, PyIterable, PyObjectRef, PyRef, PyResult,
18-
PyValue, TryFromObject, TryIntoRef, TypeProtocol,
17+
IdProtocol, IntoPyObject, ItemProtocol, PyClassImpl, PyContext, PyIterable, PyObjectRef, PyRef,
18+
PyResult, PyValue, TryFromObject, TryIntoRef, TypeProtocol,
1919
};
2020
use crate::vm::VirtualMachine;
2121

22-
use super::objint;
22+
use super::objdict::PyDict;
23+
use super::objint::{self, PyInt};
24+
use super::objnone::PyNone;
2325
use super::objsequence::PySliceableSequence;
2426
use super::objslice::PySlice;
2527
use super::objtype::{self, PyClassRef};
@@ -844,6 +846,99 @@ impl PyString {
844846
// a string is not an identifier if it has whitespace or starts with a number
845847
is_identifier_start && chars.all(|c| UnicodeXID::is_xid_continue(c))
846848
}
849+
850+
// https://docs.python.org/3/library/stdtypes.html#str.translate
851+
#[pymethod]
852+
fn translate(&self, table: PyObjectRef, vm: &VirtualMachine) -> PyResult<String> {
853+
let mut translated = String::new();
854+
// It throws a type error if it is not subscribtable
855+
vm.get_method(table.clone(), "__getitem__")?;
856+
for c in self.value.chars() {
857+
match table.get_item(c as u32, vm) {
858+
Ok(value) => {
859+
if let Some(text) = value.payload::<PyString>() {
860+
translated.extend(text.value.chars());
861+
} else if let Some(_) = value.payload::<PyNone>() {
862+
// Do Nothing
863+
} else if let Some(bigint) = value.payload::<PyInt>() {
864+
match bigint.as_bigint().to_u32().and_then(std::char::from_u32) {
865+
Some(ch) => translated.push(ch as char),
866+
None => {
867+
return Err(vm.new_value_error(format!(
868+
"character mapping must be in range(0x110000)"
869+
)));
870+
}
871+
}
872+
} else {
873+
return Err(vm.new_type_error(
874+
"character mapping must return integer, None or str".to_owned(),
875+
));
876+
}
877+
}
878+
_ => translated.push(c),
879+
}
880+
}
881+
Ok(translated)
882+
}
883+
884+
#[pymethod]
885+
fn maketrans(
886+
dict_or_str: PyObjectRef,
887+
to_str: OptionalArg<PyStringRef>,
888+
none_str: OptionalArg<PyStringRef>,
889+
vm: &VirtualMachine,
890+
) -> PyResult {
891+
let new_dict = vm.context().new_dict();
892+
if let OptionalArg::Present(to_str) = to_str {
893+
match dict_or_str.downcast::<PyString>() {
894+
Ok(from_str) => {
895+
if to_str.len(vm) == from_str.len(vm) {
896+
for (c1, c2) in from_str.value.chars().zip(to_str.value.chars()) {
897+
new_dict.set_item(c1 as u32, vm.new_int(c2 as u32), vm)?;
898+
}
899+
if let OptionalArg::Present(none_str) = none_str {
900+
for c in none_str.value.chars() {
901+
new_dict.set_item(c as u32, vm.get_none(), vm)?;
902+
}
903+
}
904+
new_dict.into_pyobject(vm)
905+
} else {
906+
Err(vm.new_value_error(
907+
"the first two maketrans arguments must have equal length".to_owned(),
908+
))
909+
}
910+
}
911+
_ => Err(vm.new_type_error(
912+
"first maketrans argument must be a string if there is a second argument"
913+
.to_owned(),
914+
)),
915+
}
916+
} else {
917+
// dict_str must be a dict
918+
match dict_or_str.downcast::<PyDict>() {
919+
Ok(dict) => {
920+
for (key, val) in dict {
921+
if let Some(num) = key.payload::<PyInt>() {
922+
new_dict.set_item(num.as_bigint().to_i32(), val, vm)?;
923+
} else if let Some(string) = key.payload::<PyString>() {
924+
if string.len(vm) == 1 {
925+
let num_value = string.value.chars().next().unwrap() as u32;
926+
new_dict.set_item(num_value, val, vm)?;
927+
} else {
928+
return Err(vm.new_value_error(
929+
"string keys in translate table must be of length 1".to_owned(),
930+
));
931+
}
932+
}
933+
}
934+
new_dict.into_pyobject(vm)
935+
}
936+
_ => Err(vm.new_value_error(
937+
"if you give only one argument to maketrans it must be a dict".to_owned(),
938+
)),
939+
}
940+
}
941+
}
847942
}
848943

849944
impl PyValue for PyString {
@@ -1119,4 +1214,30 @@ mod tests {
11191214
assert!(!PyString::from(s).istitle(&vm));
11201215
}
11211216
}
1217+
1218+
#[test]
1219+
fn str_maketrans_and_translate() {
1220+
let vm = VirtualMachine::new();
1221+
1222+
let table = vm.context().new_dict();
1223+
table
1224+
.set_item("a", vm.new_str("🎅".to_owned()), &vm)
1225+
.unwrap();
1226+
table.set_item("b", vm.get_none(), &vm).unwrap();
1227+
table
1228+
.set_item("c", vm.new_str("xda".to_owned()), &vm)
1229+
.unwrap();
1230+
let translated = PyString::maketrans(
1231+
table.into_object(),
1232+
OptionalArg::Missing,
1233+
OptionalArg::Missing,
1234+
&vm,
1235+
)
1236+
.unwrap();
1237+
let text = PyString::from("abc");
1238+
let translated = text.translate(translated, &vm).unwrap();
1239+
assert_eq!(translated, "🎅xda".to_owned());
1240+
let translated = text.translate(vm.new_int(3), &vm);
1241+
assert_eq!(translated.unwrap_err().class().name, "TypeError".to_owned());
1242+
}
11221243
}

whats_left.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ python3 not_impl_gen.py
88

99
cd ..
1010

11-
cargo run -- tests/snippets/whats_left_to_implement.py
11+
cargo run -- tests/snippets/whats_left_to_implement.py

0 commit comments

Comments
 (0)