Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions src/sort/compare.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
// The ordering options, which decide how one key is compared with another. Any of
// them may be given on the command line, and a key definition may override the
// ones set globally.
enum OrderMode {
ascii
numeric
general
human
month
version
}

struct Ordering {
mut:
mode OrderMode
ignore_blanks bool
dictionary bool
fold_case bool
ignore_nonprint bool
reverse bool
// stable turns off the last-resort comparison, so that lines which compare
// equal keep the order they came in with.
stable bool
// random is -R, which shuffles but keeps lines with equal keys together.
random bool
}

// compare_key compares a and b under one ordering, including the comparison of
// the part that follows a number, so that a non-zero result means the two keys
// are ordered and a zero result means the next key, or the last-resort
// comparison, has to decide.
fn compare_key(ordering &Ordering, a &string, b &string) int {
x := order_key(ordering, a)
y := order_key(ordering, b)

match ordering.mode {
.general, .numeric, .human {
// Only the number weighs here. GNU does not look at the text that
// follows it, which is why "1a" and "01b" sort as their whole lines
// do rather than by the a and b.
rn := compare_values(x.value, y.value)
if rn != 0 {
return flip(ordering.reverse, rn)
}
}
.ascii {
rt := compare_bytes(x.text, y.text)
if rt != 0 {
return flip(ordering.reverse, rt)
}
}
.month {
rm := compare_values(f64(x.month), f64(y.month))
if rm != 0 {
return flip(ordering.reverse, rm)
}
// A month weighs with the text after it, so "JAN" comes before "JANUARY".
rs2 := compare_bytes(x.rest, y.rest)
if rs2 != 0 {
return flip(ordering.reverse, rs2)
}
}
.version {
rv := compare_version(x.text, y.text)
if rv != 0 {
return flip(ordering.reverse, rv)
}
// A version sort compares the whole key, digits and all, so there is
// nothing left over for the last resort but the lines themselves.
return 0
}
else {
rt := compare_bytes(x.text, y.text)
if rt != 0 {
return flip(ordering.reverse, rt)
}
}
}
return 0
}

// order_key is one key as the ordering wants to see it: the characters this
// ordering weighs, and the value of the number or month that may start it.
struct OrderedKey {
text string
value f64
month int
rest string
}

// order_key applies the ordering options to s. The transformations are the ones
// GNU applies, in this order: leading blanks, then the characters -d and -i skip,
// then the case folding. A numeric ordering reads what is left.
fn order_key(ordering &Ordering, s string) OrderedKey {
mut text := s
if ordering.ignore_blanks {
text = text.trim_left(' \t\n\v\f\r')
}
if ordering.dictionary {
mut out := []u8{}
for c in text {
if c.is_digit() || (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`) || is_blank(c) {
out << c
}
}
text = out.bytestr()
}
if ordering.ignore_nonprint {
mut out := []u8{}
for c in text {
if c >= ` ` && c <= `~` {
out << c
}
}
text = out.bytestr()
}
if ordering.fold_case {
text = fold_case(text)
}

match ordering.mode {
.numeric {
n := number_prefix(text, .string)
return OrderedKey{ text: text, value: n.value, rest: n.rest }
}
.general {
n := number_prefix(text, .general)
return OrderedKey{ text: text, value: n.value, rest: n.rest }
}
.human {
n := number_prefix(text, .human)
return OrderedKey{ text: text, value: n.value, rest: n.rest }
}
.month {
month, rest := month_prefix(text)
return OrderedKey{ text: text, month: month, rest: rest }
}
else {
return OrderedKey{ text: text }
}
}
}

// compare_values orders two f64, putting a NaN before everything and an infinity
// at either end, which is the order GNU documents for -g.
fn compare_values(a f64, b f64) int {
// A NaN is the only value that is not equal to itself.
a_nan := a != a
b_nan := b != b
if a_nan {
return if b_nan { 0 } else { -1 }
}
if b_nan {
return 1
}
if a < b {
return -1
}
if a > b {
return 1
}
return 0
}

// compare_bytes is a plain byte comparison, the order LC_ALL=C gives.
fn compare_bytes(a string, b string) int {
mut i := 0
for i < a.len && i < b.len {
if a[i] != b[i] {
return if a[i] < b[i] { -1 } else { 1 }
}
i++
}
if i >= a.len && i >= b.len {
return 0
}
return if i >= a.len { -1 } else { 1 }
}

fn flip(reverse bool, result int) int {
return if reverse { -result } else { result }
}

// fold_case maps the ASCII letters to one case, so that a comparison under -f
// weighs them the same whichever case they came in.
fn fold_case(s string) string {
mut out := []u8{len: s.len, cap: s.len}
for i, c in s {
if c >= `A` && c <= `Z` {
out[i] = c + 32
} else if c >= 0xc0 && c <= 0xde && c != 0xd7 {
out[i] = c + 32
} else {
out[i] = c
}
}
return out.bytestr()
}

const month_names = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov',
'dec']

// month_prefix reads a month name at the start of s, ignoring case, and returns
// its number and the rest. A line that does not start with a month gets -1, so
// that it comes before every month, as at GNU.
fn month_prefix(s string) (int, string) {
if s.len < 3 {
return -1, s
}
mut name := []u8{cap: 3}
for i in 0 .. 3 {
c := s[i] | 0x20
if c < `a` || c > `z` {
return -1, s
}
name << c
}
folded := name.bytestr()
for m, candidate in month_names {
if folded == candidate {
return m, s[3..]
}
}
return -1, s
}
147 changes: 147 additions & 0 deletions src/sort/driver.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// The comparison the whole program is built on. It applies each key definition in
// turn, and when they all call the lines equal it falls back on comparing the lines
// themselves, which is the last-resort comparison that -s turns off.
struct Comparator {
options Options
keys []SortKey
}

// compare returns how a should be ordered against b.
fn (c &Comparator) compare(a &InputLine, b &InputLine) int {
mut result := compare_keys(c.options, c.keys, a, b)
if result != 0 {
return result
}
if !c.options.ordering.stable {
if c.options.ordering.reverse {
result = compare_bytes(b.text, a.text)
} else {
result = compare_bytes(a.text, b.text)
}
if result != 0 {
return result
}
}
// Lines that weigh the same keep the order they arrived in, which is what -s
// asks for and what -u relies on to decide which line of a run it keeps.
return compare_values(f64(a.order), f64(b.order))
}

fn make_comparator(options Options, keys []SortKey) &Comparator {
return &Comparator{
options: options
keys: keys
}
}

// compare_keys compares two lines by their keys alone, without the last-resort
// step. A zero result means the keys are equal.
//
// With no -k at all the whole line is the key, and the ordering options given on
// the command line are what weigh it.
fn compare_keys(options Options, keys []SortKey, a &InputLine, b &InputLine) int {
if keys.len == 0 {
return compare_key(&options.ordering, &a.text, &b.text)
}
for key in keys {
ak := extract_key(a.text, &key, options.separator, options.has_separator,
options.ordering.ignore_blanks)
bk := extract_key(b.text, &key, options.separator, options.has_separator,
options.ordering.ignore_blanks)
result := compare_key(&key.ordering, &ak, &bk)
if result != 0 {
return result
}
}
return 0
}

// sort_lines orders the whole input. Without -s the comparison is total, because
// of the last-resort step, so a plain sort is enough.
fn sort_lines(lines []InputLine, options Options, keys []SortKey) []InputLine {
cmp := make_comparator(sort_options(options), keys)
mut out := lines.clone()
out.sort_with_compare(fn [cmp] (a &InputLine, b &InputLine) int {
return cmp.compare(a, b)
})
return out
}

// unique_lines keeps the first line of every run that compares equal, which is
// what -u means: the lines may differ, the keys may not.
fn unique_lines(lines []InputLine, options Options, keys []SortKey) []InputLine {
if !options.unique {
return lines
}
mut out := []InputLine{}
for line in lines {
if out.len > 0 {
previous := out[out.len - 1]
// Only the keys count here. -s is deliberately not consulted: a run of
// equal keys is a run of equal keys however the lines were ordered.
if compare_keys(options, keys, &previous, &line) == 0 {
continue
}
}
out << line
}
return out
}

// check_input reports the first line that is out of order, the way GNU's -c does.
fn check_input(lines []InputLine, options Options, keys []SortKey) {
cmp := make_comparator(options, keys)
mut i := 1
for i < lines.len {
previous := lines[i - 1]
current := lines[i]
result := cmp.compare(&previous, &current)
// The comparison breaks a tie by the order the lines arrived in, so a
// non-zero result here means the later line weighs less than the earlier
// one, which is a disorder. With -u a line whose key equals the one before
// it is a disorder as well.
equal_keys := compare_keys(options, keys, &previous, &current) == 0
disordered := result > 0 || (options.unique && equal_keys)
if disordered {
if options.check == .diagnose {
println('${app_name}: ${current.file}:${current.number}: disorder: ${current.text}')
}
exit(1)
}
i++
}
}

// merge_input combines already sorted inputs the way -m does. The promise of -m is
// that each input is sorted, so the lines are merged without being sorted again,
// and a line that is out of place in its own file stays where it was.
fn merge_input(streams [][]InputLine, options Options, keys []SortKey) []InputLine {
cmp := make_comparator(options, keys)
mut out := []InputLine{}
mut pos := []int{len: streams.len, init: 0}

for {
// Find the stream whose next line weighs least. When the lines weigh the
// same the earlier stream wins, which is the order GNU merges in.
mut best := -1
for i, stream in streams {
if pos[i] >= stream.len {
continue
}
if best < 0 {
best = i
continue
}
chosen := cmp.compare(stream[pos[i]], streams[best][pos[best]])
if chosen < 0 {
best = i
}
}
if best < 0 {
break
}
out << streams[best][pos[best]]
pos[best]++
}
return out
}
Loading
Loading