is there a better way to align this column of numb

2019-09-08 10:52发布

问题:

I need to format a column of numbers in textView by displaying a column of fractional and decimal numbers and aligning them so the decimal point or forward slash characters appear in the same column like so ...

    x      1/1
    1     76.04900
    x    193.15686
    x    310.26471
    4      5/4
    x    503.42157
    6    579.47057
    x    696.57843
    x     25/16
    9    889.73529
    x   1006.84314
    11  1082.89214

This was achieved in Swift by inserting the appropriate number of leading whitespace characters and using left justification with a font where the column width is the same for each character.

To calculate the number of whitespaces, characters in the remainder or the dividend are excluded from the total number of characters in each number. This simple task involved subtracting .index(of: ) from .count and was complicated by the fact that .index(of:) returns an optional Array.Index? not an Int?

I suspect there might be a simpler way to achieve the same result and welcome any improvements to the Swift code below that might make life easier for whoever has to maintain it. The code was extracted from my project and has been tested in Playground.

Note. textMessage is a single string. It is currently displayed in the Debug area. It will eventually be displayed in a textView.

//: Playground

    import UIKit

    var tuningArray            = ["1/1", "76.04900", "193.15686", "310.26471", "5/4", "503.42157", "579.47057", "696.57843", "25/16", "889.73529", "1006.84314", "1082.89214"]
    var keyArray               = ["x", "1", "x", "x", "4", "x", "6", "x", "x", "9", "x", "11"]

    var characterCountArray    = [Int]()
    var scaleSize              = Int()
    var textMessage            = String()



    func formatTextMessage() {
    var formattedTextArray     = [String](repeating: "", count: scaleSize)

    for i in 0..<scaleSize  {
        let element            = tuningArray[i]
        countNumericCharactersBeforeDotOrSlash (s: element)
        }

    let largestStringSize      = characterCountArray.reduce(Int.min, { max($0, $1) })

    for i in 0..<scaleSize {

        let thisStringSize     = characterCountArray[i]
        let blanks             = largestStringSize - thisStringSize
        let filler             = insertWhiteSpace(count: blanks)

        formattedTextArray[i].append("\t" + keyArray[i] + "\t" + filler + tuningArray[i] + "\n")
        textMessage            += formattedTextArray[i]
        }
    print(textMessage)
    }



    func insertWhiteSpace(count: Int) -> String {
    var filler                 = ""
        for _ in 0..<count {
        filler                 = filler + " "
        }
    return filler
    }



    func countNumericCharactersBeforeDotOrSlash (s: String) {

    let fractionalNumber       = findFractionalNumber(s: s)
    let decimalNumber          = findDecimalNumber(s: s)
    var thisStringSize         = 0

    if !fractionalNumber && !decimalNumber {
        print("Error: invalid number format")
    }

    if fractionalNumber        == true {

    let pitchStringArray       = Array(s.characters)
    let endIndex               = pitchStringArray.count

    if let slashIndex          = pitchStringArray.index(of: "/") {
        let dividendFiller     = endIndex - slashIndex
        thisStringSize         = s.characters.count - dividendFiller
    }

    } else {

    if decimalNumber           == true {

        let pitchStringArray   = Array(s.characters)
        let endIndex           = pitchStringArray.count

        if let dotIndex        = pitchStringArray.index(of: ".") {
           let remainderFiller = endIndex - dotIndex
            thisStringSize     = s.characters.count - remainderFiller
            }
         }
      }
    characterCountArray.append(thisStringSize)
    }



    func findDecimalNumber(s: String?) -> Bool {
    if (s?.contains("."))      == true   {
        return true
        }
        return false
    }



    func findFractionalNumber(s: String?) -> Bool {
    if (s?.contains("/"))      == true   {
        return true
        }
        return false
    }



    scaleSize                  = tuningArray.count
    formatTextMessage()

回答1:

You could use attributed text with custom tab stops, like in this example:

import UIKit
import PlaygroundSupport


var tuningArray            = ["1/1", "76.04900", "193.15686", "310.26471", "5/4", "503.42157", "579.47057", "696.57843", "25/16", "889.73529", "1006.84314", "1082.89214"]

var keyArray    = ["x", "1", "x", "x", "4", "x", "6", "x", "x", "9", "x", "11"]
let scaleSize   = tuningArray.count
var textMessage = ""

for i in 0..<scaleSize {
    var textLine = "\t" + keyArray[i] + "\t" + tuningArray[i] + "\n"
    textMessage  += textLine
}

let paragraphStyle = NSMutableParagraphStyle()
let decimalTerminators:CharacterSet = [".", "/"]
let decimalTabOptions = [NSTabColumnTerminatorsAttributeName:decimalTerminators]
let decimalTabLocation = CGFloat(100) // TODO: Calculate...
var decimalTab = NSTextTab(textAlignment: .natural, location: decimalTabLocation, options: decimalTabOptions)
var leftTab = NSTextTab(textAlignment: .left, location: CGFloat(0.01), options: [:])
paragraphStyle.tabStops = [leftTab, decimalTab]

var a = NSAttributedString(string:textMessage,attributes: [NSParagraphStyleAttributeName: paragraphStyle])

let tv = UITextView(frame:CGRect(x:0, y:0, width:400, height:200))
tv.attributedText = a
PlaygroundPage.current.liveView = tv

The only drawback is, that you have to calculate the position of the decimal tab somehow; in the example I just use CGFloat(100) to get a nice value. But maybe you could simply live with a constant like this.



回答2:

Greg, Instead of using space to set columns use tab (\t) tree for times based on columns size you need. That will help to manage code better way with very less and powerful coding.

Thanks