| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 |
- //
- // JCChatViewUpdate.swift
- // JChat
- //
- // Created by deng on 10/04/2017.
- // Copyright © 2017 HXHG. All rights reserved.
- //
- import UIKit
- internal enum JCChatViewUpdateChangeItem {
- case insert(JCMessageType, at: Int)
- case update(JCMessageType, at: Int)
- case remove(at: Int)
- case move(at: Int, to: Int)
-
- var at: Int {
- switch self {
- case .insert(_, let at): return at
- case .update(_, let at): return at
- case .remove(let at): return at
- case .move(let at, _): return at
- }
- }
- }
- internal enum JCChatViewUpdateChange: CustomStringConvertible {
-
- case move(from: Int, to: Int)
- case update(from: Int, to: Int)
- case insert(from: Int, to: Int)
- case remove(from: Int, to: Int)
-
- var from: Int {
- switch self {
- case .move(let from, _): return from
- case .insert(let from, _): return from
- case .update(let from, _): return from
- case .remove(let from, _): return from
- }
- }
- var to: Int {
- switch self {
- case .move(_, let to): return to
- case .insert(_, let to): return to
- case .update(_, let to): return to
- case .remove(_, let to): return to
- }
- }
-
- var isMove: Bool {
- switch self {
- case .move: return true
- default: return false
- }
- }
- var isUpdate: Bool {
- switch self {
- case .update: return true
- default: return false
- }
- }
- var isRemove: Bool {
- switch self {
- case .remove: return true
- default: return false
- }
- }
- var isInsert: Bool {
- switch self {
- case .insert: return true
- default: return false
- }
- }
-
-
- var description: String {
- let from = self.from >= 0 ? "\(self.from)" : "N"
- let to = self.to >= 0 ? "\(self.to)" : "N"
-
- switch self {
- case .move: return "M\(from)/\(to)"
- case .insert: return "A\(from)/\(to)"
- case .update: return "R\(from)/\(to)"
- case .remove: return "D\(from)/\(to)"
- }
- }
-
- func offset(_ offset: Int) -> JCChatViewUpdateChange {
- let from = self.from + offset + max(min(self.from, 0), -1) * offset
- let to = self.to + offset + max(min(self.to, 0), -1) * offset
- // convert
- switch self {
- case .move: return .move(from: from, to: to)
- case .insert: return .insert(from: from, to: to)
- case .update: return .update(from: from, to: to)
- case .remove: return .remove(from: from, to: to)
- }
- }
- }
- internal class JCChatViewUpdate: NSObject {
-
-
- internal init(newData: JCChatViewData, oldData: JCChatViewData, updateItems: Array<JCChatViewUpdateChangeItem>) {
- self.newData = newData
- self.oldData = oldData
- self.updateItems = updateItems
- super.init()
- self.updateChanges = _computeItemUpdates(newData, oldData, updateItems)
- }
-
- // MARK: compute
- internal func _computeItemUpdates(_ newData: JCChatViewData, _ oldData: JCChatViewData, _ updateItems: Array<JCChatViewUpdateChangeItem>) -> Array<JCChatViewUpdateChange> {
- guard !updateItems.isEmpty else {
- return []
- }
- var allInserts: Array<(Int, JCMessageType)> = []
- var allUpdates: Array<(Int, JCMessageType)> = []
- var allRemoves: Array<(Int)> = []
- var allMoves: Array<(Int, Int)> = []
-
- // get max & min
- let (first, last) = updateItems.reduce((.max, .min)) { result, item -> (Int, Int) in
-
- switch item {
- case .move(let from, let to):
- // ignore for source equ dest
- guard abs(from - to) >= 1 else {
- return result
- }
- // move message
- allMoves.append((from, to))
- // splite to insert & remove
- if let message = _element(at: from) {
- allRemoves.append((from))
- allInserts.append((to + 1, message))
- }
- // from + 1: the selected row will change
- return (min(min(from, to + 1), result.0), max(max(from + 1, to + 1), result.1))
-
- case .remove(let index):
- // remove message
- allRemoves.append((index))
- return (min(index, result.0), max(index + 1, result.1))
-
- case .update(let message, let index):
- // update message
- allUpdates.append((index, message))
- return (min(index, result.0), max(index + 1, result.1))
-
- case .insert(let message, let index):
- // insert message
- allInserts.append((index, message))
- return (min(index, result.0), max(index, result.1))
- }
- }
- // is empty
- guard first != .max && last != .min else {
- return []
- }
- // sort
- // allInserts.sort { $0.0 < $1.0 }
- // allUpdates.sort { $0.0 < $1.0 }
- // allRemoves.sort { $0 < $1 }
- // allMoves.sort { $0.0 < $1.0 }
- let count = oldData.count
- let begin = first - 1 // prev
- let end = last + 1 // next
-
- var ii = allInserts.startIndex
- var iu = allUpdates.startIndex
- var ir = allRemoves.startIndex
- // var im = allMoves.startIndex
-
- // priority: insert > remove > update > move
-
- var items: Array<JCMessageType> = []
-
- // processing
- (first ... last).forEach { index in
- // do you need to insert the operation?
- while ii < allInserts.endIndex && allInserts[ii].0 == index {
- items.append(allInserts[ii].1)
- ii += 1
- }
- // do you need to do this?
- guard index < last && index < count else {
- return
- }
- // do you need to remove the operation?
- while ir < allRemoves.endIndex && allRemoves[ir] == index {
- // adjust previous tl-message & next tl-message, if needed
- if let content = _element(at: index - 1)?.content as? JCMessageTimeLineContent {
- content.after = nil
- }
- if let content = _element(at: index + 1)?.content as? JCMessageTimeLineContent {
- content.before = nil
- }
- // move to next operator(prevent repeat operation)
- while ir < allRemoves.endIndex && allRemoves[ir] == index {
- ir += 1
- }
- // can't update or copy
- return
- }
- // do you need to update the operation?
- while iu < allUpdates.endIndex && allUpdates[iu].0 == index {
- let message = allUpdates[iu].1
- // updating
- items.append(message)
- // adjust previous tl-message & next tl-message, if needed
- if let content = _element(at: index - 1)?.content as? JCMessageTimeLineContent {
- content.after = message
- }
- if let content = _element(at: index + 1)?.content as? JCMessageTimeLineContent {
- content.before = message
- }
- // move to next operator(prevent repeat operation)
- while iu < allUpdates.endIndex && allUpdates[iu].0 == index {
- iu += 1
- }
- // can't copy
- return
- }
- // copy
- items.append(oldData[index])
- }
- // convert messages and replace specify message
- let newItems = items as [JCMessageType]
- let convertedItems = _convert(messages: newItems, first: _element(at: begin), last: _element(at: end - 1))
- let selectedRange = max(begin, 0) ..< min(end, count)
- let selectedItems = oldData.subarray(with: selectedRange)
-
- // compute index paths
- let start = selectedRange.lowerBound
- // lcs
- let diff = _diff(selectedItems, convertedItems).map { $0.offset(start) }
- // ::
- // replace
- newData.elements = oldData.elements
- newData.replaceSubrange(selectedRange, with: convertedItems)
-
- return diff
- }
-
- // MARK: convert message
-
- private func _convert(messages elements: [JCMessageType], first: JCMessageType?, last: JCMessageType?) -> [JCMessageType] {
- // merge
- let elements = [first].flatMap({ $0 }) + elements + [last].flatMap({ $0 })
- // processing
- return (0 ..< elements.count).reduce(NSMutableArray(capacity: elements.count * 2)) { result, index in
- let current = elements[index]
- result.add(current)
- // continue
- return result
- } as! [JCMessageType]
- }
- internal func _element(at index: Int) -> JCMessageType? {
- guard index >= 0 && index < oldData.count else {
- return nil
- }
- return oldData[index]
- }
-
- // MARK: compare
- private func _equal<T: JCMessageType>(_ lhs: T, _ rhs: T) -> Bool {
- return lhs.identifier == rhs.identifier && lhs.options.state == rhs.options.state
- }
-
- private func _diff<T: JCMessageType>(_ src: Array<T>, _ dest: Array<T>) -> Array<JCChatViewUpdateChange> {
-
- let len1 = src.count
- let len2 = dest.count
-
- var c = [[Int]](repeating: [Int](repeating: 0, count: len2 + 1), count: len1 + 1)
-
- // lcs + 动态规划
- for i in 1 ..< len1 + 1 {
- for j in 1 ..< len2 + 1 {
- if _equal(src[i - 1], (dest[j - 1])) {
- c[i][j] = c[i - 1][j - 1] + 1
- } else {
- c[i][j] = max(c[i - 1][j], c[i][j - 1])
- }
- }
- }
-
- var i = len1
- var j = len2
-
- var rms: Array<(from: Int, to: Int)> = []
- var adds: Array<(from: Int, to: Int)> = []
-
- // create the optimal path
- repeat {
- guard i != 0 else {
- // the remaining is add
- while j > 0 {
- adds.append((from: i - 1, to: j - 1))
- j -= 1
- }
- break
- }
- guard j != 0 else {
- // the remaining is remove
- while i > 0 {
- rms.append((from: i - 1, to: j - 1))
- i -= 1
- }
- break
- }
- guard !_equal(src[i - 1], (dest[j - 1])) else {
- // no change, ignore
- i -= 1
- j -= 1
- continue
- }
- // check the weight
- if c[i - 1][j] > c[i][j - 1] {
- // is remove
- rms.append((from: i - 1, to: j - 1))
- i -= 1
- } else {
- // is add
- adds.append((from: i - 1, to: j - 1))
- j -= 1
- }
- } while i > 0 || j > 0
-
- var results: Array<JCChatViewUpdateChange> = []
- results.reserveCapacity(rms.count + adds.count)
-
- // move(f,t): f = remove(f), t = insert(t), new move(f,t): f = remove(f), t = insert(f)
- // update(f,t): f = remove(f), t = insert(t), new update(f,t): f = remove(f), t = insert(f)
-
- // automatic merge delete and update items
- results.append(contentsOf: rms.map({ item in
- let from = item.from
- let delElement = src[from]
- // can't merge to move item?
- if let addIndex = adds.index(where: { _equal(dest[$0.to], delElement) }) {
- let addItem = adds.remove(at: addIndex)
- return .move(from: from, to: addItem.to)
- }
- // can't merge to update item?
- if let addIndex = adds.index(where: { $0.to == from }) {
- let addItem = adds[addIndex]
- let addElement = dest[addItem.to]
- // the same type is allowed to merge
- if type(of: delElement.content) == type(of: addElement.content) {
- adds.remove(at: addIndex)
- return .update(from: from, to: addItem.to)
- }
- }
- return .remove(from: item.from, to: -1)
- }))
- // automatic merge insert items
- results.append(contentsOf: adds.map({ item in
- return .insert(from: -1, to: item.to)
- }))
-
- // sort
- return results.sorted { $0.from < $1.from }
- }
-
- // MARK: property
- internal let newData: JCChatViewData
- internal let oldData: JCChatViewData
-
- internal let updateItems: Array<JCChatViewUpdateChangeItem>
- internal var updateChanges: Array<JCChatViewUpdateChange>?
-
- internal static var minimuxTimeInterval: TimeInterval = 60
- }
|