github.com/xyproto/orbiton/v2@v2.65.12-0.20240516144430-e10a419274ec/sortstrings.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  )
     8  
     9  // Word is a type for a string in a list of strings, that may be quoted
    10  type Word struct {
    11  	s            string
    12  	singleQuoted bool
    13  	doubleQuoted bool
    14  }
    15  
    16  // Words is a slice of Word
    17  type Words []Word
    18  
    19  // Len helps make Words sortable.
    20  // Len is the number of elements in the collection.
    21  func (ws Words) Len() int {
    22  	return len(ws)
    23  }
    24  
    25  // Less helps make Words sortable.
    26  // Less reports whether the element with
    27  // index i should sort before the element with index j.
    28  func (ws Words) Less(i, j int) bool {
    29  	// This did not provide a good sorting order for strings:
    30  	// return ws[i].s < ws[j].s
    31  
    32  	// This worked out, but is probably quite a bit slower:
    33  	twoStrings := []string{ws[i].s, ws[j].s}
    34  	sort.Strings(twoStrings)
    35  	return twoStrings[0] == ws[i].s
    36  }
    37  
    38  // Swap helps make Words sortable.
    39  // Swap swaps the elements with indexes i and j.
    40  func (ws Words) Swap(i, j int) {
    41  	ws[i], ws[j] = ws[j], ws[i]
    42  }
    43  
    44  // sortStrings tries to identify and sort a given list of strings in a string.
    45  // The strings are expected to be surrounded by either () or {},
    46  // or that the entire given string is a list of strings.
    47  // The strings may be space separated or comma separated (consistently).
    48  // The strings may be single quoted, double quoted or none (may be non-consistent).
    49  func sortStrings(line string) (string, error) {
    50  	trimmedLine := strings.TrimSpace(line)
    51  
    52  	// TODO: Bake the ({[ detection into the rune loop below
    53  
    54  	// This check is very basic and will fail at strings like:
    55  	// example=[[ "hello (0)", "hello (1)" ]]
    56  	surroundedByCurly := strings.Count(trimmedLine, "{") == 1 && strings.Count(trimmedLine, "}") == 1
    57  	surroundedByPar := strings.Count(trimmedLine, "(") == 1 && strings.Count(trimmedLine, ")") == 1
    58  	surroundedBySquareBrackets := strings.Count(trimmedLine, "[") == 1 && strings.Count(trimmedLine, "]") == 1
    59  
    60  	// Find the "center" string (a list of strings within {} or () on the current line)
    61  	center := ""
    62  	if surroundedByCurly {
    63  		fields := strings.SplitN(trimmedLine, "{", 2)
    64  		if len(fields) != 2 {
    65  			return line, fmt.Errorf("curly brackets have an unusual order: %v", fields)
    66  		}
    67  		fields2 := strings.SplitN(fields[1], "}", 2)
    68  		if len(fields2) != 2 {
    69  			return line, fmt.Errorf("curly brackets have an unusual order: %v", fields2)
    70  		}
    71  		center = fields2[0]
    72  	} else if surroundedByPar {
    73  		fields := strings.SplitN(trimmedLine, "(", 2)
    74  		if len(fields) != 2 {
    75  			return line, fmt.Errorf("parentheses have an unusual order %v", fields)
    76  		}
    77  		fields2 := strings.SplitN(fields[1], ")", 2)
    78  		if len(fields2) != 2 {
    79  			return line, fmt.Errorf("parentheses have an unusual order %v", fields2)
    80  		}
    81  		center = fields2[0]
    82  	} else if surroundedBySquareBrackets {
    83  		fields := strings.SplitN(trimmedLine, "[", 2)
    84  		if len(fields) != 2 {
    85  			return line, fmt.Errorf("square brackets have an unusual order %v", fields)
    86  		}
    87  		fields2 := strings.SplitN(fields[1], "]", 2)
    88  		if len(fields2) != 2 {
    89  			return line, fmt.Errorf("square brackets have an unusual order %v", fields2)
    90  		}
    91  		center = fields2[0]
    92  	} else {
    93  		// Assume that the entire given string is a list of strings, without surrounding (), {} or []
    94  		center = trimmedLine
    95  	}
    96  
    97  	// Okay, we have a "center" string containing a list of strings, that may be quoted, may be comma separated
    98  	// The only thing sure to be consistent is either commas or spaces
    99  	inSingleQuote := false
   100  	inDoubleQuote := false
   101  	commaCount := 0
   102  	spaceCount := 0
   103  
   104  	// Loop over the runes in the "center" string to count the commas
   105  	// and spaces that are not within single or double quotes.
   106  	for _, r := range center {
   107  		switch r {
   108  		case '\'': // single quote
   109  			if !inDoubleQuote {
   110  				inSingleQuote = !inSingleQuote
   111  			}
   112  		case '"': // double quote
   113  			if !inSingleQuote {
   114  				inDoubleQuote = !inDoubleQuote
   115  			}
   116  		case ',':
   117  			if !inSingleQuote && !inDoubleQuote {
   118  				commaCount++
   119  			}
   120  		case ' ':
   121  			if !inSingleQuote && !inDoubleQuote {
   122  				spaceCount++
   123  			}
   124  		}
   125  	}
   126  
   127  	// Are we dealing with comma-separated or space-separated strings?
   128  	// TODO: This will not work if the strings contains spaces
   129  	commaSeparated := commaCount >= spaceCount
   130  
   131  	// Split the string into a []string
   132  	var fields []string
   133  	if commaSeparated {
   134  		fields = strings.Split(center, ",")
   135  	} else {
   136  		fields = strings.Split(center, " ")
   137  	}
   138  
   139  	// Convert the string fields to Words
   140  	words := make(Words, len(fields))
   141  	for i, field := range fields {
   142  		trimmedElement := strings.TrimSpace(field)
   143  		// Remove the trailing comma after the word, if any
   144  		if strings.HasSuffix(trimmedElement, ",") {
   145  			trimmedElement = strings.TrimSpace(trimmedElement[:len(trimmedElement)-1])
   146  		}
   147  		var w Word
   148  		// Prepare a Word struct, depending on how this trimmed element is quoted
   149  		if strings.HasPrefix(trimmedElement, "'") && strings.HasSuffix(trimmedElement, "'") {
   150  			w.s = trimmedElement[1 : len(trimmedElement)-1]
   151  			w.singleQuoted = true
   152  			// fmt.Println("SINGLE QUOTED:", w.s)
   153  		} else if strings.HasPrefix(trimmedElement, "\"") && strings.HasSuffix(trimmedElement, "\"") {
   154  			w.s = trimmedElement[1 : len(trimmedElement)-1]
   155  			w.doubleQuoted = true
   156  			// fmt.Println("DOUBLE QUOTED:", w.s)
   157  		} else {
   158  			w.s = trimmedElement
   159  			// fmt.Println("NOT QUOTED:", w.s)
   160  		}
   161  		// Save the Word
   162  		words[i] = w
   163  	}
   164  
   165  	// fmt.Println("WORDS", words)
   166  
   167  	// Sort the Words
   168  	sort.Sort(words)
   169  
   170  	// Join the words to a center string with the same type of separation and quoting as the original string
   171  	var sb strings.Builder
   172  	lastIndex := len(words) - 1
   173  	for i, word := range words {
   174  		if word.singleQuoted { // single quoted
   175  			sb.WriteRune('\'')
   176  			sb.WriteString(word.s)
   177  			sb.WriteRune('\'')
   178  		} else if word.doubleQuoted { // double quoted
   179  			sb.WriteRune('"')
   180  			sb.WriteString(word.s)
   181  			sb.WriteRune('"')
   182  		} else { // bare
   183  			sb.WriteString(word.s)
   184  		}
   185  		if i == lastIndex {
   186  			// Break before adding a ", " or " " suffix to the string
   187  			break
   188  		}
   189  		if commaSeparated {
   190  			sb.WriteRune(',')
   191  		}
   192  		// Write a space after the comma, or just a space if the strings are space separated.
   193  		// This covers both.
   194  		sb.WriteRune(' ')
   195  	}
   196  	newCenter := sb.String()
   197  
   198  	// Okay, now replace the old list of strings with the new one, once
   199  	return strings.Replace(line, center, newCenter, 1), nil
   200  }
   201  
   202  // SortStrings tries to find and sort a list of strings on the current line.
   203  // The strings are expected to be surrounded by either () or {},
   204  // or that the entire given string is a list of strings.
   205  // The strings may be space separated or comma separated (consistently).
   206  // The strings may be single quoted, double quoted or none (may be non-consistent).
   207  func (e *Editor) SortStrings() error {
   208  	// Sort the strings in the current line, return an error if there are problems
   209  	newCurrentLine, err := sortStrings(e.CurrentLine())
   210  	if err != nil {
   211  		return err
   212  	}
   213  	// Use the sorted line and return
   214  	e.SetLine(e.DataY(), newCurrentLine)
   215  	return nil
   216  }