【问题标题】:iOS: String. Get line and column number from absolute position and vice versaiOS:字符串。从绝对位置获取行号和列号,反之亦然
【发布时间】:2019-03-01 17:02:04
【问题描述】:

假设我有一个包含混合 CRLFCRLF 换行符分隔符的文本。

像这样:"\n \n Lorem \r Ipsum \n is \r\n simply \n dummy \r\n text of \n the printing \r and typesetting industry. \n \n"

我正在将此文本加载到简单的文本编辑器 (NSTextView/UITextView)。 视觉上换行符看起来是一样的;只是一个新行。

我可以在简单的文本编辑器中浏览文本、选择文本、剪切、复制、粘贴……

问题:如何从absolute 字符位置(即选择NSRange)获取linecolumn 编号?另外,如何从已知的linecolumn 号码中获取absolute 字符位置?

谢谢!


更新 1

  • linecolumn 数字 - 简单表示光标位置。
  • linecolumn 编号 - 具有基于一个的编号。
  • absolute 字符位置 - 具有基于的编号。

当前解决方案的示例代码。它从absolute 字符位置计算linecolumn 数字,反之亦然。但它不会重新计算文本更改的映射。

struct TextString {

   struct Cursor {
      let line: Int
      let column: Int
   }

   struct Mapping {
      let lineNumber: Int
      let lineLength: Int
      let absolutePosition: Int

      fileprivate var absoluteStart: Int {
         return absolutePosition - lineLength
      }
   }

   let string: String
   private (set) var mappings: [Mapping] = []

   init(string: String) {
      self.string = string
      mappings = setupMappings()
   }
}

extension TextString {

   func cursor(from position: Int) -> Cursor? {
      guard position > 0 else {
         return nil
      }
      guard let mapping = mappings.first(where: { $0.absolutePosition >= position && $0.absoluteStart <= position }) else {
         return nil
      }
      let result = Cursor(line: mapping.lineNumber, column: position - mapping.absoluteStart)
      return result
   }

   func position(from cursor: Cursor) -> Int? {
      guard let line = mappings.element(at: cursor.line - 1) else {
         return nil
      }
      guard line.lineLength >= cursor.column else {
         return nil
      }
      let result = line.absoluteStart + cursor.column
      return result
   }
}

extension TextString {

   private func setupMappings() -> [Mapping] {
      var mappings: [Mapping] = []
      var line = 1
      var previousAbsolutePosition = 0
      var delta = 0
      let scanner = Scanner(string: string)
      scanner.charactersToBeSkipped = nil
      while !scanner.isAtEnd {
         if scanner.scanUpToCharacters(from: .newlines) != nil {
            let charactersLocation = scanner.scanLocation - delta
            if let newLines = scanner.scanCharacters(from: .newlines) {
               for index in 0..<newLines.count {
                  let absolutePosition = charactersLocation + 1 + index // `+1` is newLine itself
                  mappings.append(Mapping(lineNumber: line, lineLength: absolutePosition - previousAbsolutePosition,
                                          absolutePosition: absolutePosition))
                  previousAbsolutePosition = absolutePosition
                  line += 1
               }
               delta = scanner.scanLocation - previousAbsolutePosition
            } else {
               // Only happens when we at last line withot newline.
               let absolutePosition = charactersLocation
               mappings.append(Mapping(lineNumber: line, lineLength: absolutePosition - previousAbsolutePosition,
                                       absolutePosition: absolutePosition))
               line += 1
               previousAbsolutePosition = charactersLocation
            }
         } else if let newLines = scanner.scanCharacters(from: .newlines) { // Text begins with new lines.
            for index in 0..<newLines.count {
               let absolutePosition = 1 + index // `+1` is newLine itself
               mappings.append(Mapping(lineNumber: line, lineLength: absolutePosition - previousAbsolutePosition,
                                       absolutePosition: absolutePosition))
               previousAbsolutePosition = absolutePosition
               line += 1
            }
            delta = scanner.scanLocation - previousAbsolutePosition
         }
      }
      assert(previousAbsolutePosition == string.count)
      return mappings
   }
}

更新 2:RegEx 版本。

private func setupMappingsUsingRegex() throws -> [Mapping] {
   if string.isEmpty {
      return []
   }
   var mappings: [Mapping] = []
   let regex = try NSRegularExpression(pattern: "(\\r\\n)|(\\n)|(\\r)")
   let matches = regex.matches(in: string, range: NSRange(location: 0, length: string.unicodeScalars.count))
   var line = 1
   var previousAbsolutePosition = 0
   var delta = 0

   // String without any newline.
   if matches.isEmpty {
      let mapping = Mapping(lineNumber: 1, lineLength: string.count, absolutePosition: string.count)
      mappings.append(mapping)
      return mappings
   }

   for match in matches {
      let absolutePosition = match.range.location - delta + 1
      let mapping = Mapping(lineNumber: line, lineLength: absolutePosition - previousAbsolutePosition,
                            absolutePosition: absolutePosition)
      mappings.append(mapping)
      delta += match.range.length - 1
      previousAbsolutePosition = absolutePosition
      line += 1
   }

   // Rest of the string without newline at the end.
   if previousAbsolutePosition < string.count {
      let mapping = Mapping(lineNumber: line, lineLength: string.count - previousAbsolutePosition,
                            absolutePosition: string.count)
      mappings.append(mapping)
      previousAbsolutePosition = string.count
   }
   assert(previousAbsolutePosition == string.count)
   return mappings
}

性能:22400 个字符(200 行)分析 1000 次。

  • 正则表达式:5.120 秒
  • 扫描仪:6.603 秒

【问题讨论】:

  • 我会说使用stackoverflow.com/questions/31746223/…,计算\r\n(首先)的数量,然后是\n 和`r",您应该能够从由“0 到 absoluteLocation”组成的子字符串,与迭代获得 absoluteLocation 的方式相同?我不确定“行”和“列”的定义,但您可能想举个例子,比如说“s” simply.
  • 混合行结尾是没有意义的。我相信文本视图只识别\n。您应该将行尾规范化为 \n ,然后解决您的问题。

标签: ios swift macos


【解决方案1】:

我建议您使用正则表达式来分隔您的字符串。如果您看到\n\r\r\n,假设您想拆分一个子字符串,那么正则表达式将类似于

var content: String = <Your text here>
let regex = try! NSRegularExpression(pattern: "(\\n)|(\\r)|(\\r\\n)")
let matchs = regex.matches(in: content, range: NSRange(location: 0, length: content.count)).map{(content as NSString).substring(with: $0.range)}

然后您可以在匹配的结果中循环并获取索引和范围等

【讨论】:

  • 不幸的是 components(separatedBy: .newlines) 2 个不可见字符 \r\n 并且其余计算的索引将在 吃掉 换行数上错位:0跨度>
  • @LeoDabus 考虑到unicodeScalars 将起作用。是的。
【解决方案2】:

这对我来说是为了获得行号:

let content: String = <YourString>
let selectionRange: NSRange = <YourTextRange>
let regex = try! NSRegularExpression(pattern: "\n", options: [])
let lineNumber = regex.numberOfMatches(in: content, options: [], range: NSMakeRange(0, nsRange.location)) + 1

您可以自定义正则表达式以匹配您想要的任何类型的换行符。

要获取列号,您可以获取行范围,然后从选择范围中减去它的开头:

let lineRange = content.lineRange(for: selectionRange.location)
let column = selectionRange.location - lineRange.location

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-05-21
    • 2011-08-09
    • 2015-12-09
    • 2012-05-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多