From b449707021c753973fbc0165f20690e5b8cdbd45 Mon Sep 17 00:00:00 2001 From: appflowy Date: Sat, 31 Jul 2021 20:53:45 +0800 Subject: [PATCH] add flowy ot --- rust-lib/Cargo.toml | 1 + rust-lib/flowy-ot/Cargo.toml | 13 + rust-lib/flowy-ot/src/attributes.rs | 97 ++++++ rust-lib/flowy-ot/src/delta.rs | 443 ++++++++++++++++++++++++++ rust-lib/flowy-ot/src/errors.rs | 12 + rust-lib/flowy-ot/src/lib.rs | 4 + rust-lib/flowy-ot/src/operation.rs | 103 ++++++ rust-lib/flowy-ot/tests/helper/mod.rs | 46 +++ rust-lib/flowy-ot/tests/op_test.rs | 161 ++++++++++ 9 files changed, 880 insertions(+) create mode 100644 rust-lib/flowy-ot/Cargo.toml create mode 100644 rust-lib/flowy-ot/src/attributes.rs create mode 100644 rust-lib/flowy-ot/src/delta.rs create mode 100644 rust-lib/flowy-ot/src/errors.rs create mode 100644 rust-lib/flowy-ot/src/lib.rs create mode 100644 rust-lib/flowy-ot/src/operation.rs create mode 100644 rust-lib/flowy-ot/tests/helper/mod.rs create mode 100644 rust-lib/flowy-ot/tests/op_test.rs diff --git a/rust-lib/Cargo.toml b/rust-lib/Cargo.toml index 2f39b9ac10..e8b04407d2 100644 --- a/rust-lib/Cargo.toml +++ b/rust-lib/Cargo.toml @@ -15,6 +15,7 @@ members = [ "flowy-observable", "flowy-document", "flowy-editor", + "flowy-ot", ] [profile.dev] diff --git a/rust-lib/flowy-ot/Cargo.toml b/rust-lib/flowy-ot/Cargo.toml new file mode 100644 index 0000000000..280a9a3953 --- /dev/null +++ b/rust-lib/flowy-ot/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "flowy-ot" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bytecount = "0.6.0" + +[dev-dependencies] +criterion = "0.3" +rand = "0.7.3" diff --git a/rust-lib/flowy-ot/src/attributes.rs b/rust-lib/flowy-ot/src/attributes.rs new file mode 100644 index 0000000000..e6315bfeb2 --- /dev/null +++ b/rust-lib/flowy-ot/src/attributes.rs @@ -0,0 +1,97 @@ +use std::collections::{hash_map::RandomState, HashMap}; + +#[derive(Debug, Clone, Default, PartialEq)] +pub struct Attributes { + inner: HashMap, +} + +impl Attributes { + pub fn new() -> Self { + Attributes { + inner: HashMap::new(), + } + } + + pub fn remove_empty_value(&mut self) { self.inner.retain((|_, v| v.is_empty())); } + + pub fn extend(&mut self, other: Attributes) { self.inner.extend(other.inner); } +} + +impl std::convert::From> for Attributes { + fn from(attributes: HashMap) -> Self { + Attributes { inner: attributes } + } +} + +impl std::ops::Deref for Attributes { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { &self.inner } +} + +impl std::ops::DerefMut for Attributes { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner } +} + +pub fn compose_attributes( + mut a: Attributes, + b: Attributes, + keep_empty: bool, +) -> Option { + a.extend(b); + let mut result = a; + if !keep_empty { + result.remove_empty_value() + } + + return if result.is_empty() { + None + } else { + Some(result) + }; +} + +pub fn transform_attributes(a: Attributes, b: Attributes, priority: bool) -> Option { + if a.is_empty() { + return Some(b); + } + + if b.is_empty() { + return None; + } + + if !priority { + return Some(b); + } + + let result = b.iter().fold(Attributes::new(), |mut attributes, (k, v)| { + if a.contains_key(k) == false { + attributes.insert(k.clone(), v.clone()); + } + attributes + }); + Some(result) +} + +pub fn invert_attributes(attr: Option, base: Option) -> Attributes { + let attr = attr.unwrap_or(Attributes::new()); + let base = base.unwrap_or(Attributes::new()); + + let base_inverted = base + .iter() + .fold(Attributes::new(), |mut attributes, (k, v)| { + if base.get(k) != attr.get(k) && attr.contains_key(k) { + attributes.insert(k.clone(), v.clone()); + } + attributes + }); + + let inverted = attr.iter().fold(base_inverted, |mut attributes, (k, v)| { + if base.get(k) != attr.get(k) && !base.contains_key(k) { + attributes.insert(k.clone(), "".to_owned()); + } + attributes + }); + + return inverted; +} diff --git a/rust-lib/flowy-ot/src/delta.rs b/rust-lib/flowy-ot/src/delta.rs new file mode 100644 index 0000000000..375a13a450 --- /dev/null +++ b/rust-lib/flowy-ot/src/delta.rs @@ -0,0 +1,443 @@ +use crate::{errors::OTError, operation::*}; +use bytecount::num_chars; +use std::{cmp::Ordering, error::Error, fmt, iter::FromIterator}; + +#[derive(Clone, Debug, PartialEq)] +pub struct Delta { + pub ops: Vec, + pub base_len: usize, + pub target_len: usize, +} + +impl Default for Delta { + fn default() -> Self { + Self { + ops: Vec::new(), + base_len: 0, + target_len: 0, + } + } +} + +impl FromIterator for Delta { + fn from_iter>(ops: T) -> Self { + let mut operations = Delta::default(); + for op in ops { + operations.add(op); + } + operations + } +} + +impl Delta { + #[inline] + pub fn with_capacity(capacity: usize) -> Self { + Self { + ops: Vec::with_capacity(capacity), + base_len: 0, + target_len: 0, + } + } + + fn add(&mut self, op: OpType) { + match op { + OpType::Delete(i) => self.delete(i), + OpType::Insert(s) => self.insert(&s), + OpType::Retain(i) => self.retain(i), + } + } + + pub fn delete(&mut self, n: u64) { + if n == 0 { + return; + } + self.base_len += n as usize; + + if let Some(operation) = self.ops.last_mut() { + if operation.ty.is_delete() { + operation.delete(n); + return; + } + } + + self.ops.push(OperationBuilder::delete(n).build()); + } + + pub fn insert(&mut self, s: &str) { + if s.is_empty() { + return; + } + self.target_len += num_chars(s.as_bytes()); + let new_last = match self + .ops + .iter_mut() + .map(|op| &mut op.ty) + .collect::>() + .as_mut_slice() + { + [.., OpType::Insert(s_last)] => { + *s_last += &s; + return; + }, + [.., OpType::Insert(s_pre_last), OpType::Delete(_)] => { + *s_pre_last += s; + return; + }, + [.., op_last @ OpType::Delete(_)] => { + let new_last = op_last.clone(); + *(*op_last) = OpType::Insert(s.to_owned()); + new_last + }, + _ => OpType::Insert(s.to_owned()), + }; + self.ops.push(OperationBuilder::new(new_last).build()); + } + + pub fn retain(&mut self, n: u64) { + if n == 0 { + return; + } + self.base_len += n as usize; + self.target_len += n as usize; + + if let Some(operation) = self.ops.last_mut() { + if operation.ty.is_retain() { + operation.retain(n); + return; + } + } + + self.ops.push(OperationBuilder::retain(n).build()); + } + + /// Merges the operation with `other` into one operation while preserving + /// the changes of both. Or, in other words, for each input string S and a + /// pair of consecutive operations A and B. + /// `apply(apply(S, A), B) = apply(S, compose(A, B))` + /// must hold. + /// + /// # Error + /// + /// Returns an `OTError` if the operations are not composable due to length + /// conflicts. + pub fn compose(&self, other: &Self) -> Result { + if self.target_len != other.base_len { + return Err(OTError); + } + + let mut new_delta = Delta::default(); + let mut ops1 = self.ops.iter().cloned(); + let mut ops2 = other.ops.iter().cloned(); + + let mut maybe_op1 = ops1.next(); + let mut maybe_op2 = ops2.next(); + loop { + match ( + &maybe_op1.as_ref().map(|o| &o.ty), + &maybe_op2.as_ref().map(|o| &o.ty), + ) { + (None, None) => break, + (Some(OpType::Delete(i)), _) => { + new_delta.delete(*i); + maybe_op1 = ops1.next(); + }, + (_, Some(OpType::Insert(s))) => { + new_delta.insert(s); + maybe_op2 = ops2.next(); + }, + (None, _) | (_, None) => { + return Err(OTError); + }, + (Some(OpType::Retain(i)), Some(OpType::Retain(j))) => match i.cmp(&j) { + Ordering::Less => { + new_delta.retain(*i); + maybe_op2 = Some(OperationBuilder::retain(*j - *i).build()); + maybe_op1 = ops1.next(); + }, + std::cmp::Ordering::Equal => { + new_delta.retain(*i); + maybe_op1 = ops1.next(); + maybe_op2 = ops2.next(); + }, + std::cmp::Ordering::Greater => { + new_delta.retain(*j); + maybe_op1 = Some(OperationBuilder::retain(*i - *j).build()); + maybe_op2 = ops2.next(); + }, + }, + (Some(OpType::Insert(s)), Some(OpType::Delete(j))) => { + match (num_chars(s.as_bytes()) as u64).cmp(j) { + Ordering::Less => { + maybe_op2 = Some( + OperationBuilder::delete(*j - num_chars(s.as_bytes()) as u64) + .build(), + ); + maybe_op1 = ops1.next(); + }, + Ordering::Equal => { + maybe_op1 = ops1.next(); + maybe_op2 = ops2.next(); + }, + Ordering::Greater => { + maybe_op1 = Some( + OperationBuilder::insert(s.chars().skip(*j as usize).collect()) + .build(), + ); + maybe_op2 = ops2.next(); + }, + } + }, + (Some(OpType::Insert(s)), Some(OpType::Retain(j))) => { + match (num_chars(s.as_bytes()) as u64).cmp(j) { + Ordering::Less => { + new_delta.insert(s); + maybe_op2 = Some( + OperationBuilder::retain(*j - num_chars(s.as_bytes()) as u64) + .build(), + ); + maybe_op1 = ops1.next(); + }, + Ordering::Equal => { + new_delta.insert(s); + maybe_op1 = ops1.next(); + maybe_op2 = ops2.next(); + }, + Ordering::Greater => { + let chars = &mut s.chars(); + new_delta.insert(&chars.take(*j as usize).collect::()); + maybe_op1 = Some(OperationBuilder::insert(chars.collect()).build()); + maybe_op2 = ops2.next(); + }, + } + }, + (Some(OpType::Retain(i)), Some(OpType::Delete(j))) => match i.cmp(&j) { + Ordering::Less => { + new_delta.delete(*i); + maybe_op2 = Some(OperationBuilder::delete(*j - *i).build()); + maybe_op1 = ops1.next(); + }, + Ordering::Equal => { + new_delta.delete(*j); + maybe_op2 = ops2.next(); + maybe_op1 = ops1.next(); + }, + Ordering::Greater => { + new_delta.delete(*j); + maybe_op1 = Some(OperationBuilder::retain(*i - *j).build()); + maybe_op2 = ops2.next(); + }, + }, + }; + } + Ok(new_delta) + } + + /// Transforms two operations A and B that happened concurrently and + /// produces two operations A' and B' (in an array) such that + /// `apply(apply(S, A), B') = apply(apply(S, B), A')`. + /// This function is the heart of OT. + /// + /// # Error + /// + /// Returns an `OTError` if the operations cannot be transformed due to + /// length conflicts. + pub fn transform(&self, other: &Self) -> Result<(Self, Self), OTError> { + if self.base_len != other.base_len { + return Err(OTError); + } + + let mut a_prime = Delta::default(); + let mut b_prime = Delta::default(); + + let mut ops1 = self.ops.iter().cloned(); + let mut ops2 = other.ops.iter().cloned(); + + let mut maybe_op1 = ops1.next(); + let mut maybe_op2 = ops2.next(); + loop { + match ( + &maybe_op1.as_ref().map(|o| &o.ty), + &maybe_op2.as_ref().map(|o| &o.ty), + ) { + (None, None) => break, + (Some(OpType::Insert(s)), _) => { + a_prime.insert(s); + b_prime.retain(num_chars(s.as_bytes()) as _); + maybe_op1 = ops1.next(); + }, + (_, Some(OpType::Insert(s))) => { + a_prime.retain(num_chars(s.as_bytes()) as _); + b_prime.insert(s); + maybe_op2 = ops2.next(); + }, + (None, _) => { + return Err(OTError); + }, + (_, None) => { + return Err(OTError); + }, + (Some(OpType::Retain(i)), Some(OpType::Retain(j))) => { + match i.cmp(&j) { + Ordering::Less => { + a_prime.retain(*i); + b_prime.retain(*i); + maybe_op2 = Some(OperationBuilder::retain(*j - *i).build()); + maybe_op1 = ops1.next(); + }, + Ordering::Equal => { + a_prime.retain(*i); + b_prime.retain(*i); + maybe_op1 = ops1.next(); + maybe_op2 = ops2.next(); + }, + Ordering::Greater => { + a_prime.retain(*j); + b_prime.retain(*j); + maybe_op1 = Some(OperationBuilder::retain(*i - *j).build()); + maybe_op2 = ops2.next(); + }, + }; + }, + (Some(OpType::Delete(i)), Some(OpType::Delete(j))) => match i.cmp(&j) { + Ordering::Less => { + maybe_op2 = Some(OperationBuilder::delete(*j - *i).build()); + maybe_op1 = ops1.next(); + }, + Ordering::Equal => { + maybe_op1 = ops1.next(); + maybe_op2 = ops2.next(); + }, + Ordering::Greater => { + maybe_op1 = Some(OperationBuilder::delete(*i - *j).build()); + maybe_op2 = ops2.next(); + }, + }, + (Some(OpType::Delete(i)), Some(OpType::Retain(j))) => { + match i.cmp(&j) { + Ordering::Less => { + a_prime.delete(*i); + maybe_op2 = Some(OperationBuilder::retain(*j - *i).build()); + maybe_op1 = ops1.next(); + }, + Ordering::Equal => { + a_prime.delete(*i); + maybe_op1 = ops1.next(); + maybe_op2 = ops2.next(); + }, + Ordering::Greater => { + a_prime.delete(*j); + maybe_op1 = Some(OperationBuilder::delete(*i - *j).build()); + maybe_op2 = ops2.next(); + }, + }; + }, + (Some(OpType::Retain(i)), Some(OpType::Delete(j))) => { + match i.cmp(&j) { + Ordering::Less => { + b_prime.delete(*i); + maybe_op2 = Some(OperationBuilder::delete(*j - *i).build()); + maybe_op1 = ops1.next(); + }, + Ordering::Equal => { + b_prime.delete(*i); + maybe_op1 = ops1.next(); + maybe_op2 = ops2.next(); + }, + Ordering::Greater => { + b_prime.delete(*j); + maybe_op1 = Some(OperationBuilder::retain(*i - *j).build()); + maybe_op2 = ops2.next(); + }, + }; + }, + } + } + + Ok((a_prime, b_prime)) + } + + /// Applies an operation to a string, returning a new string. + /// + /// # Error + /// + /// Returns an error if the operation cannot be applied due to length + /// conflicts. + pub fn apply(&self, s: &str) -> Result { + if num_chars(s.as_bytes()) != self.base_len { + return Err(OTError); + } + let mut new_s = String::new(); + let chars = &mut s.chars(); + for op in &self.ops { + match &op.ty { + OpType::Retain(retain) => { + for c in chars.take(*retain as usize) { + new_s.push(c); + } + }, + OpType::Delete(delete) => { + for _ in 0..*delete { + chars.next(); + } + }, + OpType::Insert(insert) => { + new_s += insert; + }, + } + } + Ok(new_s) + } + + /// Computes the inverse of an operation. The inverse of an operation is the + /// operation that reverts the effects of the operation + pub fn invert(&self, s: &str) -> Self { + let mut inverse = Delta::default(); + let chars = &mut s.chars(); + for op in &self.ops { + match &op.ty { + OpType::Retain(retain) => { + inverse.retain(*retain); + for _ in 0..*retain { + chars.next(); + } + }, + OpType::Insert(insert) => { + inverse.delete(num_chars(insert.as_bytes()) as u64); + }, + OpType::Delete(delete) => { + inverse.insert(&chars.take(*delete as usize).collect::()); + }, + } + } + inverse + } + + /// Checks if this operation has no effect. + #[inline] + pub fn is_noop(&self) -> bool { + match self + .ops + .iter() + .map(|op| &op.ty) + .collect::>() + .as_slice() + { + [] => true, + [OpType::Retain(_)] => true, + _ => false, + } + } + + /// Returns the length of a string these operations can be applied to + #[inline] + pub fn base_len(&self) -> usize { self.base_len } + + /// Returns the length of the resulting string after the operations have + /// been applied. + #[inline] + pub fn target_len(&self) -> usize { self.target_len } + + /// Returns the wrapped sequence of operations. + #[inline] + pub fn ops(&self) -> &[Operation] { &self.ops } +} diff --git a/rust-lib/flowy-ot/src/errors.rs b/rust-lib/flowy-ot/src/errors.rs new file mode 100644 index 0000000000..623cc96a79 --- /dev/null +++ b/rust-lib/flowy-ot/src/errors.rs @@ -0,0 +1,12 @@ +use std::{error::Error, fmt}; + +#[derive(Clone, Debug)] +pub struct OTError; + +impl fmt::Display for OTError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "incompatible lengths") } +} + +impl Error for OTError { + fn source(&self) -> Option<&(dyn Error + 'static)> { None } +} diff --git a/rust-lib/flowy-ot/src/lib.rs b/rust-lib/flowy-ot/src/lib.rs new file mode 100644 index 0000000000..f3439fbaa7 --- /dev/null +++ b/rust-lib/flowy-ot/src/lib.rs @@ -0,0 +1,4 @@ +mod attributes; +pub mod delta; +pub mod errors; +pub mod operation; diff --git a/rust-lib/flowy-ot/src/operation.rs b/rust-lib/flowy-ot/src/operation.rs new file mode 100644 index 0000000000..53a707568d --- /dev/null +++ b/rust-lib/flowy-ot/src/operation.rs @@ -0,0 +1,103 @@ +use crate::attributes::Attributes; +use std::{ + cmp::Ordering, + collections::{hash_map::RandomState, HashMap}, + ops::Deref, +}; + +#[derive(Clone, Debug, PartialEq)] +pub struct Operation { + pub ty: OpType, + pub attrs: Attributes, +} + +impl Operation { + pub fn delete(&mut self, n: u64) { self.ty.delete(n); } + + pub fn retain(&mut self, n: u64) { self.ty.retain(n); } + + pub fn is_plain(&self) -> bool { self.attrs.is_empty() } + + pub fn is_noop(&self) -> bool { + match self.ty { + OpType::Retain(_) => true, + _ => false, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum OpType { + Delete(u64), + Retain(u64), + Insert(String), +} + +impl OpType { + pub fn is_delete(&self) -> bool { + match self { + OpType::Delete(_) => true, + _ => false, + } + } + + pub fn is_retain(&self) -> bool { + match self { + OpType::Retain(_) => true, + _ => false, + } + } + + pub fn is_insert(&self) -> bool { + match self { + OpType::Insert(_) => true, + _ => false, + } + } + + pub fn delete(&mut self, n: u64) { + debug_assert_eq!(self.is_delete(), true); + if let OpType::Delete(n_last) = self { + *n_last += n; + } + } + + pub fn retain(&mut self, n: u64) { + debug_assert_eq!(self.is_retain(), true); + if let OpType::Retain(i_last) = self { + *i_last += n; + } + } +} + +pub struct OperationBuilder { + ty: OpType, + attrs: Attributes, +} + +impl OperationBuilder { + pub fn new(ty: OpType) -> OperationBuilder { + OperationBuilder { + ty, + attrs: Attributes::default(), + } + } + + pub fn retain(n: u64) -> OperationBuilder { OperationBuilder::new(OpType::Retain(n)) } + + pub fn delete(n: u64) -> OperationBuilder { OperationBuilder::new(OpType::Delete(n)) } + + pub fn insert(s: String) -> OperationBuilder { OperationBuilder::new(OpType::Insert(s)) } + + pub fn with_attrs(mut self, attrs: Attributes) -> OperationBuilder { + self.attrs = attrs; + self + } + + pub fn build(self) -> Operation { + Operation { + ty: self.ty, + attrs: self.attrs, + } + } +} diff --git a/rust-lib/flowy-ot/tests/helper/mod.rs b/rust-lib/flowy-ot/tests/helper/mod.rs new file mode 100644 index 0000000000..c587518016 --- /dev/null +++ b/rust-lib/flowy-ot/tests/helper/mod.rs @@ -0,0 +1,46 @@ +use flowy_ot::delta::Delta; +use rand::{prelude::*, Rng as WrappedRng}; + +pub struct Rng(StdRng); + +impl Default for Rng { + fn default() -> Self { Rng(StdRng::from_rng(thread_rng()).unwrap()) } +} + +impl Rng { + pub fn from_seed(seed: [u8; 32]) -> Self { Rng(StdRng::from_seed(seed)) } + + pub fn gen_string(&mut self, len: usize) -> String { + (0..len).map(|_| self.0.gen::()).collect() + } + + pub fn gen_delta(&mut self, s: &str) -> Delta { + let mut op = Delta::default(); + loop { + let left = s.chars().count() - op.base_len(); + if left == 0 { + break; + } + let i = if left == 1 { + 1 + } else { + 1 + self.0.gen_range(0, std::cmp::min(left - 1, 20)) + }; + match self.0.gen_range(0.0, 1.0) { + f if f < 0.2 => { + op.insert(&self.gen_string(i)); + }, + f if f < 0.4 => { + op.delete(i as u64); + }, + _ => { + op.retain(i as u64); + }, + } + } + if self.0.gen_range(0.0, 1.0) < 0.3 { + op.insert(&("1".to_owned() + &self.gen_string(10))); + } + op + } +} diff --git a/rust-lib/flowy-ot/tests/op_test.rs b/rust-lib/flowy-ot/tests/op_test.rs new file mode 100644 index 0000000000..4c1d3dd9fd --- /dev/null +++ b/rust-lib/flowy-ot/tests/op_test.rs @@ -0,0 +1,161 @@ +mod helper; + +use crate::helper::Rng; +use bytecount::num_chars; +use flowy_ot::{ + delta::Delta, + operation::{OpType, OperationBuilder}, +}; + +#[test] +fn lengths() { + let mut delta = Delta::default(); + assert_eq!(delta.base_len, 0); + assert_eq!(delta.target_len, 0); + delta.retain(5); + assert_eq!(delta.base_len, 5); + assert_eq!(delta.target_len, 5); + delta.insert("abc"); + assert_eq!(delta.base_len, 5); + assert_eq!(delta.target_len, 8); + delta.retain(2); + assert_eq!(delta.base_len, 7); + assert_eq!(delta.target_len, 10); + delta.delete(2); + assert_eq!(delta.base_len, 9); + assert_eq!(delta.target_len, 10); +} +#[test] +fn sequence() { + let mut delta = Delta::default(); + delta.retain(5); + delta.retain(0); + delta.insert("lorem"); + delta.insert(""); + delta.delete(3); + delta.delete(0); + assert_eq!(delta.ops.len(), 3); +} + +#[test] +fn apply() { + for _ in 0..1000 { + let mut rng = Rng::default(); + let s = rng.gen_string(50); + let delta = rng.gen_delta(&s); + assert_eq!(num_chars(s.as_bytes()), delta.base_len); + assert_eq!(delta.apply(&s).unwrap().chars().count(), delta.target_len); + } +} +#[test] +fn invert() { + for _ in 0..1000 { + let mut rng = Rng::default(); + let s = rng.gen_string(50); + let delta_a = rng.gen_delta(&s); + let delta_b = delta_a.invert(&s); + assert_eq!(delta_a.base_len, delta_b.target_len); + assert_eq!(delta_a.target_len, delta_b.base_len); + assert_eq!(delta_b.apply(&delta_a.apply(&s).unwrap()).unwrap(), s); + } +} +#[test] +fn empty_ops() { + let mut delta = Delta::default(); + delta.retain(0); + delta.insert(""); + delta.delete(0); + assert_eq!(delta.ops.len(), 0); +} +#[test] +fn eq() { + let mut delta_a = Delta::default(); + delta_a.delete(1); + delta_a.insert("lo"); + delta_a.retain(2); + delta_a.retain(3); + let mut delta_b = Delta::default(); + delta_b.delete(1); + delta_b.insert("l"); + delta_b.insert("o"); + delta_b.retain(5); + assert_eq!(delta_a, delta_b); + delta_a.delete(1); + delta_b.retain(1); + assert_ne!(delta_a, delta_b); +} +#[test] +fn ops_merging() { + let mut delta = Delta::default(); + assert_eq!(delta.ops.len(), 0); + delta.retain(2); + assert_eq!(delta.ops.len(), 1); + assert_eq!(delta.ops.last(), Some(&OperationBuilder::retain(2).build())); + delta.retain(3); + assert_eq!(delta.ops.len(), 1); + assert_eq!(delta.ops.last(), Some(&OperationBuilder::retain(5).build())); + delta.insert("abc"); + assert_eq!(delta.ops.len(), 2); + assert_eq!( + delta.ops.last(), + Some(&OperationBuilder::insert("abc".to_owned()).build()) + ); + delta.insert("xyz"); + assert_eq!(delta.ops.len(), 2); + assert_eq!( + delta.ops.last(), + Some(&OperationBuilder::insert("abcxyz".to_owned()).build()) + ); + delta.delete(1); + assert_eq!(delta.ops.len(), 3); + assert_eq!(delta.ops.last(), Some(&OperationBuilder::delete(1).build())); + delta.delete(1); + assert_eq!(delta.ops.len(), 3); + assert_eq!(delta.ops.last(), Some(&OperationBuilder::delete(2).build())); +} +#[test] +fn is_noop() { + let mut delta = Delta::default(); + assert!(delta.is_noop()); + delta.retain(5); + assert!(delta.is_noop()); + delta.retain(3); + assert!(delta.is_noop()); + delta.insert("lorem"); + assert!(!delta.is_noop()); +} +#[test] +fn compose() { + for _ in 0..1000 { + let mut rng = Rng::default(); + let s = rng.gen_string(20); + let a = rng.gen_delta(&s); + let after_a = a.apply(&s).unwrap(); + assert_eq!(a.target_len, num_chars(after_a.as_bytes())); + + let b = rng.gen_delta(&after_a); + let after_b = b.apply(&after_a).unwrap(); + assert_eq!(b.target_len, num_chars(after_b.as_bytes())); + + let ab = a.compose(&b).unwrap(); + assert_eq!(ab.target_len, b.target_len); + let after_ab = ab.apply(&s).unwrap(); + assert_eq!(after_b, after_ab); + } +} +#[test] +fn transform() { + for _ in 0..1000 { + let mut rng = Rng::default(); + let s = rng.gen_string(20); + let a = rng.gen_delta(&s); + let b = rng.gen_delta(&s); + let (a_prime, b_prime) = a.transform(&b).unwrap(); + let ab_prime = a.compose(&b_prime).unwrap(); + let ba_prime = b.compose(&a_prime).unwrap(); + let after_ab_prime = ab_prime.apply(&s).unwrap(); + let after_ba_prime = ba_prime.apply(&s).unwrap(); + assert_eq!(ab_prime, ba_prime); + assert_eq!(after_ab_prime, after_ba_prime); + } +}