My first code golf post, apologies for any mistakes...

Context

In rock climbing (bouldering specifically), the V/Vermin (USA) climbing grades start at 'VB' (the easiest grade), and then go 'V0', 'V0+', 'V1', 'V2', 'V3', 'V4', 'V5' etc. up to 'V17' (the hardest grade).

You will take as input a list/array of climbing grades and you have to to return or print a list/array of the grades sorted from easiest to hardest.

If the input is empty, return an empty data structure; otherwise the input will always be a valid.

Test cases

Input | Output
[] |  []
['V1'] |  ['V1']
['V7', 'V12', 'V1'] | ['V1', 'V7', 'V12']
['V13', 'V14', 'VB', 'V0'] |  ['VB', 'V0', 'V13', 'V14']
['V0+', 'V0', 'V16', 'V2', 'VB', 'V6'] | ['VB', 'V0', 'V0+', 'V2', 'V6', 'V16']


This is a challenge.

• Next time, post this on the sandbox to get feedback before posting it. Secondly, should you really answer your own challenge? Commented Aug 2, 2017 at 8:55
• Will duplicate grades appear in the input? Commented Aug 2, 2017 at 9:01
• @Mr.Xcoder No duplicates Commented Aug 2, 2017 at 9:02
• Welcome to PPCG! Pretty clear and nice for a first question. (y) Commented Aug 2, 2017 at 9:33
• Very nice first question! The answers it has lead to are so varying and creative. :)
– lynn
Commented Aug 2, 2017 at 10:04

Python 2, 58 54 bytes

lambda x:sorted(x,key=lambda y,B10=0:eval(y[1:]+'10'))


Try it online!

How it works

y         y[1:]+'10'   eval(y[1:]+'10')
=======================================
VB        B10          0  (a variable we defined)
V0        010          8  (an octal literal)
V0+       0+10         10
V1        110          110
V2        210          210
...       ...          ...
V17       1710         1710

• Looks like porting this to ES6 doesn’t beat Arnauld’s approach: a=>a.sort((a,b,B10=0)=>(g=s=>eval(s.slice(1)+10))(a)>g(b)) is 58 bytes.
– lynn
Commented Aug 2, 2017 at 10:23
• a=>a.sort((a,b)=>(g=s=>eval(s.slice(B10=1)+10))(a)-g(b)) is 2 bytes shorter, but that's still too long. Commented Aug 2, 2017 at 10:27
• @GB I think it was valid, but now it’s definitely valid.
– lynn
Commented Aug 2, 2017 at 10:45
• Why use '10' and not something shorter? For example '2' saves 2 bytes.
– G B
Commented Aug 2, 2017 at 13:31
• @GB The trick is to trigger the translation from the octal notation "010" to 8 as decimal for "V0". With 2, you'd get "02" = 2, which is the same as "0+2". Commented Aug 2, 2017 at 13:39

JavaScript (ES6) / Firefox, 53 bytes

a=>a.sort((a,b)=>(g=s=>parseInt(s,32)%334+s)(a)>g(b))


Test cases

For Firefox:

let f =

a=>a.sort((a,b)=>(g=s=>parseInt(s,32)%334+s)(a)>g(b))

console.log(JSON.stringify(f([])))
console.log(JSON.stringify(f(['V1'])))
console.log(JSON.stringify(f(['V7', 'V12', 'V1'])))
console.log(JSON.stringify(f(['V13', 'V14', 'VB', 'V0'])))
console.log(JSON.stringify(f(['V0+', 'V0', 'V16', 'V2', 'VB', 'V6'])))

For Chrome or Edge (+4 bytes):

let f =

a=>a.sort((a,b)=>(g=s=>parseInt(s,32)%334+s)(a)>g(b)||-1)

console.log(JSON.stringify(f([])))
console.log(JSON.stringify(f(['V1'])))
console.log(JSON.stringify(f(['V7', 'V12', 'V1'])))
console.log(JSON.stringify(f(['V13', 'V14', 'VB', 'V0'])))
console.log(JSON.stringify(f(['V0+', 'V0', 'V16', 'V2', 'VB', 'V6'])))

How?

We apply 3 successive transformations that lead to lexicographically comparable strings.

s     | Base32 -> dec. | MOD 334 | +s
------+----------------+---------+---------
"VB"  |           1003 |       1 | "1VB"
"V0"  |            992 |     324 | "324V0"
"V0+" |            992 |     324 | "324V0+"
"V1"  |            993 |     325 | "325V1"
"V2"  |            994 |     326 | "326V2"
"V3"  |            995 |     327 | "327V3"
"V4"  |            996 |     328 | "328V4"
"V5"  |            997 |     329 | "329V5"
"V6"  |            998 |     330 | "330V6"
"V7"  |            999 |     331 | "331V7"
"V8"  |           1000 |     332 | "332V8"
"V9"  |           1001 |     333 | "333V9"
"V10" |          31776 |      46 | "46V10"
"V11" |          31777 |      47 | "47V11"
"V12" |          31778 |      48 | "48V12"
"V13" |          31779 |      49 | "49V13"
"V14" |          31780 |      50 | "50V14"
"V15" |          31781 |      51 | "51V15"
"V16" |          31782 |      52 | "52V16"
"V17" |          31783 |      53 | "53V17"

• Did you come up with the base conversion/modulo idea? Brilliant! Commented Aug 2, 2017 at 13:14
• @kamoroso94 FWIW, here is the code that I wrote to find the base and the modulo. It gives some other possible answers (with m < 1000). Commented Aug 2, 2017 at 13:28
• I tried a=>a.sort((a,b)=>(g=s=>parseInt(s,32)%334+s)(a)>g(b)) on Chrome, it doesn't give the correct answer to f(["VB","V0","V0+","V1","V2","V3","V4","V5","V6","V7","V8","V9","V10","V11","V12","V13","V14","V15","V16","V17"]) I'm not sure why; the edge-compatible version works fine on chrome.
– Ra8
Commented Aug 2, 2017 at 15:46
• @Ra8 Ah, yes. It appears to be unstable for Chrome as well. Returning a boolean from a sort() callback is just a hack that happens to work in Firefox, but we really are supposed to return a signed value. Thanks for your feedback! Commented Aug 2, 2017 at 16:02

Husk, 5 bytes

ÖiÖm±


Try it online! The results are printed one per line, but internally this is a function that takes and returns a list of strings.

Explanation

This is surprisingly similar to Martin's Retina answer. First we do Öm±, meaning "order by mapping is-digit". This puts VB, V0 and V0+ in the correct order, since they are compared as [0,0], [0,1] and [0,1,0]. Next we do Öi, meaning "order by integer value". Given a string, i returns the first sequence of digits occurring in it as an integer, or 0 if one is not found. The three strings above are all mapped to 0 and the sort is stable, so they will be in the correct order in the output.

Retina, 14 bytes

B
!
O
!
B
O#


Try it online!

Explanation

B
!


Replace B with ! so that the lexicographic order of grades puts VB (or then V!) in front of all the numeric grades.

O


Sort all input lines lexicographically. This doesn't give the right result but it does order V! < V0 < V0+ correctly.

!
B


Turn V! back into VB.

O#


Sort the lines numerically. Retina simply looks for the first decimal number in a string to determines its sort key. If there is no number (such as for VB), it sets the value to 0. That means all of VB, V0 and V0+ have the same sort key. But Retina's sort is stable and we've already put them in the correct relative order.

V, 3 bytes

Úún


Try it online!

How does it work?

ú   # Sort on...
n  #   the first decimal number on the line


This command is almost a valid solution, since every line that can't be sorted by numbers (AKA, VB) will be placed at the beginning, without the order changed. However, since it's only looking at numbers, it can't distinguish between V0 and V0+. Since Vim uses a stable sort, whichever of these came first will remain first after sorting it. So...

Ú   # Sort lexicographically (will place 'V0' before 'V0+')
ú  # Sort by...
n #   The first number on the line

• How appropriate that V does well on this challenge :P Commented Aug 3, 2017 at 0:35

C#, 1218382 83 bytes

Saved 39 bytes thanks to TheLethalCoder and LiefdeWen

a=>a.OrderBy(x=>x[1]>65?-1:x=="V0+"?0.5:int.Parse(x.Remove(0,1)))


Try it online!

Bytecount includes using System.Linq.

How?

• Gets an array of strings as input.
• If the input is equal to VB, set the value to -1, if it's equal to VB0+, set the value to 0.
• Order the input based on the number value that comes after the V.

Might be a bit of a hack, but it works! :)

• 94 bytes Commented Aug 2, 2017 at 9:22
• @LiefdeWen You don't need the ToArray() an IOrderedEnumerable should be fine. Commented Aug 2, 2017 at 9:24
• Sorry accidently removed System.Linq reference, fixed it Commented Aug 2, 2017 at 9:24
• @TheLethalCoder You are right as always, 84 bytes Commented Aug 2, 2017 at 9:26
• @LiefdeWen .Remove(0,1) for additional -1 byte :) Commented Aug 2, 2017 at 9:27

Ruby, 52 42 41 bytes

->x{[?B,0,"0+",*1..17].map{|a|"V#{a}"}&x}


Try it online!

How it works:

Turn the problem around, produce the full sorted list, then get the intersection with our input.

Thanks Lynn for saving 1 byte.

• Clever! ->x{[?B,0,"0+",*1..17].map{|a|"V#{a}"}&x} saves a byte.
– lynn
Commented Aug 2, 2017 at 11:12

Pyth, 16 bytes

Port of Python answer by OP.

o|hx"B00+"tN+3st


05AB1E, 1713 8 bytes

Σþï}„VB†


Try it online!

• Ha... † is better than D"VB"åiÁ by a loong-shot. Commented Aug 16, 2017 at 1:36

Jelly, 9 bytes

Ḋv-.F+LµÞ


A monadic link taking a list of lists of characters and returning the sorted list.

Try it online! (the footer formats the result nicely)

How?

Ḋv-.F+LµÞ - Link: list of lists of characters
µÞ - sort by key:
Ḋ         -   dequeue (remove the 'V' from the item)
-.      -   literal -0.5
v        -   evaluate as Jelly code with argument -0.5
-   ...this means VB and V0+ become -0.5
-      (to binary and addition respectively)
-      while others become their literal numbers
F     -   flatten
+L   -   add the length of the item
-   ...'VB', 'V0', 'V0+', 'V1', 'V2'... -> 1.5, 2, 2.5, 3, 4, ...


f l=['V':x|x<-"B":"0":"0+":map show[1..17],_:y<-l,x==y]


Try it online!

To kick things off here is my Python 3 solution... Apologies, posted this too soon against convention, now re-posting...

Python 3, 69 67 bytes

lambda l:sorted(l,key=lambda x:'B00+'.find(x[1:])+1or int(x[1:])+3)


Try it online!

• It's discouraged to answer your own challenge immediately. Give some time for some other people to answer, at least 48 hours, probably longer. Commented Aug 2, 2017 at 8:56
• @TheLethalCoder Oh right, on Stack Overflow such behavior is encouraged! Should I delete my answer? Commented Aug 2, 2017 at 9:01
• @Chris_Rands Yes, I suggest you to delete it. Commented Aug 2, 2017 at 9:01
• @Downvoter: Downvoting a new member for doing something they didn't know was frowned upon ain't cool; much better to simply point out that they shouldn't, as Lethal did. Commented Aug 2, 2017 at 9:02
• Note though if someone doesn't post your solution you're welcome to do so. After waiting of course Commented Aug 2, 2017 at 9:05

Swift 3, 102 bytes

var r={String((Int($0,radix:32) ?? 992)%334)+$0};func f(l:[String]){print(l.sorted(by:{r($0)<r($1)}))}


This is a function. You can call it as such:

f(l:["V0","VB","V13","V0+"])


Try it online!

How does this work?

This is basically a port of the amazing Javascript answer by @Arnauld, but optimized for Swift.

It maps each of the values to lexicographically orderable Strings as shown in the table below:

Initial String -> Result

V1  ->  325V1
V10 ->  46V10
V11 ->  47V11
V12 ->  48V12
V13 ->  49V13
V14 ->  50V14
V15 ->  51V15
V16 ->  52V16
V17 ->  53V17
V2  ->  326V2
V3  ->  327V3
V4  ->  328V4
V5  ->  329V5
V6  ->  330V6
V7  ->  331V7
V8  ->  332V8
V9  ->  333V9
V0+ ->  324V0+
V0  ->  324V0
VB  ->  1VB


Code Explanation

• String((Int($0,radix:32) ?? 992)%334) - Converts each String from a base-32 Number to Decimal. In case the value is "V0+", the call to Int(_:radix:) will return nil, and we take the value of "V0", 992. We additionally take the result of mod 334, and finally convert it to String. • +$0 - Adds the current value to the String created above. For instance, if the String is V9, the function above returns 333 and we add V9, resulting in 333V9.

• var r={...} - Declares a variable r to an anonymous closure, because it saves lots of bytes since it's used twice.

• func f(l:[String]) - Defines a function f with a parameter l, a list of Strings.

• print(l.sorted(by:{r($0)<r($1)})) - Prints the result of sorting the given list, with the key being the variable r defined above.

PowerShell, 45 bytes

param($a)'B',0,'0+'+1..17|%{"V$_"}|?{$_-in$a}


Try it online!

Uses the same process as G B's Ruby answer to construct the full argument list in sorted order, then select out those that are -in the input list.

=ArrayFormula(If(A1="","",Sort(Transpose(Split(A1,",")),Transpose(IfError(Find(Split(A1,","),"VBV0V0+"),Value(Mid(Split(A1,","),2,3))+9)),1)))


Input is a string in A1 with each entry separated by a comma.
Output is the formula's cell plus the n-1 cells below it where n is the number of entries in A1.

It's a long, messy formula so let's unpack it.

• If(A1="","",~) fixes the null input. Without this, an empty input returns a #VALUE! error because the Split function doesn't work on empty inputs.
• Transpose(Split(A1,",")) splits A1 at the commas and transposes it into a column because the Sort function only works on columns.
• Transpose(IfError(Find(),Value()+9)) is breaks into these pieces:
• Find(Split(A1,","),"VBV0V0+") tries to find each parameter in that string. These first three are the only ones that must be sorted as strings so we use Find to get their sort order.
• Value(Mid(Split(A1,","),2,3))+9 gets the numerical value of the grade. This only matters for V1 and higher so they sort numerically just fine. The +9 at the end is to ensure V1 comes after V0+ since its Find value would be 5. Technically, then, only +5 is required but it costs me no more bytes to make extra double sure it sorts correctly.
• IfError(Find(~),Value(~)) returns the Find value if the string was found (i.e., the grade is VB, V0, or V0+). If it can't be found, it returns the numerical value of the grade plus nine.
• Transpose(IfError(~)) again turns it into a column so Sort can use it.
• Sort(Transpose(Split(~)),Transpose(IfError(Find(~),Value(~)+9)),1) wraps it all up by sorting the split input using the custom sort order ascending.
• ArrayFormula(~) wraps the entire thing so it returns the results as an array instead of just returning the first value in that array. This is what causes the formula in one cell to fill the cells below it, too.
• I think this is the first time I've ever seen Google Sheets used. Kudos to you, and +1! Commented Aug 3, 2017 at 15:42

Bash + coreutils, 21

tr B .|sort -V|tr . B


GNU sort's -Version sorting mode almost does what we want. Switch the B for a . and we're done.

import Data.List
f"VB"=[]
f(_:'1':[a])='X':[a]
f x=x
sortOn f


Try it online!

f is a function that converts climbing grades to strings that can be compared. If converts VB to be the empty string so it gets the highest priority, it then replaces V1 with X in strings that are three long to lower the priority of V10-V17. For the remainder we do nothing.

To sort the list we use Data.Lists's sortOn function (as suggested by Lynn) to create a point-free function.

• That’s just g=sortOn f, which is also in Data.List.
– lynn
Commented Aug 3, 2017 at 12:18
• Also, f(_:'1':a)='X':a saves 4 bytes!
– lynn
Commented Aug 3, 2017 at 12:22
• @Lynn The first suggestion works, however the second one does not, I need [a] otherwise V1 will be pattern matched which is the problem I am trying to circumvent. Commented Aug 3, 2017 at 14:25

R, 45 bytes

l=paste0('V',c('B','0','0+',1:17));l[l%in%x]


How does this work?

• Assign the correctly ordered vector of grades to 'l';
• Use 'paste0' instead of 'paste' to avoiding making a 'sep=""' argument;
• Index 'l' based on matches of 'l' in your input vector of mixed, unsorted grades.

Python2, 77 bytes

sorted(input(),key=lambda s:float(s[1:].replace("B","-1").replace("+",".5")))

• I think this counts as a snippet! Because you are neither printing the result nor this is a function definition. You can make it into a lambda or print the result though. Commented Aug 2, 2017 at 10:11
• @officialaimm nice try but does not work if V0+ s before V0. Commented Aug 2, 2017 at 10:45

Jelly, 17 11 bytes

fØD
ṢÇÞÇV$Þ  Try it online! TXR Lisp: 45 bytes (op sort @1 :(ret@(mod(toint @1 32)334)@1))  Run: 1> (op sort @1 :(ret@(mod(toint @1 32)334)@1)) #<interpreted fun: lambda (#:arg-01-0168 . #:rest-0167)> 2> [*1 ()] nil 3> [*1 (list "V0+" "V0" "V16" "V2" "VB" "V6")] ("VB" "V0" "V0+" "V2" "V6" "V16")  Perl 5, 56 + 1 (-a) = 57 bytes map{$i=$_;s/V//;$a[/B/?0:/^0/?length:$_+2]=$i}@F;say"@a"


Try it online!