Home > Software design >  How do I sort a txt list using a batch file?
How do I sort a txt list using a batch file?

Time:07-22

I'm trying to set up a very basic scoreboard system which should work on every Windows machine without having to install anything (like Python). I'm not sure if a batch file is the way to go there, since it's really outdated, but I just went for it here. This is what I'm trying to do:

  • Have a script that lets the operator enter a name and a score
  • Sort all of the scores
  • Export the top 5 players as a separate txt file (e.g. PointsTopPlayer1.txt, NameTopPlayer1.txt, PointsTopPlayer2.txt, NameTopPlayer2.txt, etc.)

This is what I have so far. A batch-script that let's the user enter a name and some points.

:start
@echo off
cls
echo Please enter data
echo.
set /p _player=name:
:inputpoints
set /p _points=points:
echo %_points%| findstr /r "^[1-9][0-9]*$">nul
if not %errorlevel% equ 0 goto invalid
(echo=%_points%,%_player%)>> data.txt
echo Data saved.
timeout 2 > nul
goto start

:invalid
echo Please enter a valid number.
timeout 1 > nul
goto inputpoints

Now my next guess would be to add a line in the script that sorts the data.txt list, which currently looks like this:

500,Bob
390,Thomas
650,Sam
100,Nick
20,Olivia

Altough it looks like there is no real way to sort numbers except for the sort command, which is not working in this case, because it sorts the numbers alphabetically, which puts 100 before 20. I saw this script online, but I have no clue how to use it to be honest. Next step would be exporting all of the data in seperate files (for the top 5 players), which is something I have no idea how to pull off.

Any ideas appreciated!

CodePudding user response:

If as you say PowerShell is an option for you, you could do this:

$scores = while ($true) {
    cls
    $player = Read-Host "Please enter the name of a player. (an empty input exits)"
    if ([string]::IsNullOrWhiteSpace($player)) { 
        break  # exit the outer loop
    }
    while ($true) {
        $points = Read-Host "Please enter the points for player $player"
        if ($points -match '^\d $') {   # check if input is numeric
            break  # exit the inner loop
        }
        else {
            Write-Host "Please enter a valid number" -ForegroundColor Red
            Start-Sleep 1
        }
    }
    # output an object with player name and point
    [PsCustomObject]@{
        Player = $player
        Points = [int]$points  # convert to int so the sorting later will be numeric
    }
}

# if you like, save this all as structured csv file you can open in Excel
$scores | Export-Csv -Path 'X:\Somewhere\scoredata.csv' -NoTypeInformation -UseCulture

# now sort and select the top 5 players
$top5 = $scores | Sort-Object -Property Points -Descending | Select-Object -First 5

# write the textfiles, but it is totally unclear what you want as content of these files..
for ($i = 0; $i -lt $top5.Count; $i  ) {
    $content = '{0},{1}' -f $top5[$i].Points, $top5[$i].Player
    # file PointsTopPlayerX.txt
    $content | Set-Content -Path ('X:\Somewhere\{0}TopPlayer{1}.txt' -f $top5[$i].Points, ($i   1))
    # file NameTopPlayerX.txt
    $content | Set-Content -Path ('X:\Somewhere\{0}TopPlayer{1}.txt' -f $top5[$i].Player, ($i   1))
}

CodePudding user response:

Seeing as this is a comma separated list, you can use Import-Csv as long as you assign it headers:

@"
500,Bob
390,Thomas
650,Sam
100,Nick
20,Olivia
"@ | ConvertFrom-Csv -Header "Numbers","Name" | Sort-Object -Property {[int]$_.Numbers}

I use ConvertFrom-Csv for this example but, Import-Csv will work the same. The next step is using a scriptblock in our Sort-Object to assign the newly created Numbers column as type int to sort by.

CodePudding user response:

An idea : Add 100000000 to _points before saving it. That way, you would have for instance

100000500,Bob
100000390,Thomas

Which will sort quite nicely. If you were analysing the data, you could use

for/f "tokens=1*delims=," %%b in (filename) do (
 set /a points=%%b-100000000
 echo !points! %%c
)

Noting that you would need to invoke delayedexpansion as the value of points is being varied within the code block. See Stephan's DELAYEDEXPANSION link

To get the first n names to a top-scores list is then simple:

for /f "tokens=1,2,*delims=[]," %%u in ('type yourfile^|sort^|find /n "1" ') do if %%u leq 5 (set /a points=%%v-100000000
 echo !points!,%%w
)

Again, you'd need delayedexpansion.

Or, you could follow Stephan's advice

(
for/f "tokens=1*delims=," %%b in (filename) do (
 set /a points=%%b 100000000
 echo !points!,%%c
)
)>tempfile
(
for /f "tokens=1,*delims=," %%b in ('type tempfile^|sort ') do (set /a points=%%b-100000000
 echo !points!,%%c
)
)>filename

which would replace filename with a sorted version

Note that ((codeblock))>filename redirects output to a new version of filename.

But overall, the key to tackling this problem in batch is delayedexpansion.

CodePudding user response:

A Batch approach that uses nested for loops in conjunction with a temporary array to effect sorting via conditional assessment:

@Echo off & Cls

For /f "delims=" %%e in ('echo Prompt $E^|Cmd')Do Set "\E=%%e"

    Setlocal EnableDelayedExpansion
    Set "Players[i]=0"

    REM substitue %~f0 below with the filepath of the input data
    For /f "tokens=1,2 Delims=," %%G in ('%SystemRoot%\System32\Findstr.exe /RC:"^[0-9][0-9]*,[a-zA-Z][a-zA-Z]*[ ]*[a-zA-Z][a-zA-Z]*$" "%~f0"')Do (
        %= increment array index          =% Set /A "Players[i] =1"
        
        REM index included in variable name to guard against duplicate names
        Set "Player[!Players[i]!]=%%H (Player.!Players[i]!)"
        %= Assign Score to indexed player =% Set "%%H (Player.!Players[i]!)=%%G"

    )

    Call:OutputSorted unsorted

    Call:SortArray Player Players[i] LEQ
    Call:OutputSorted Sorted Low to high 

    Call:SortArray Player Players[i] GEQ
    Call:OutputSorted Sorted High to Low


Goto:eof

:SortArray ======================= <Element_VarName> <Element_Index_VarName> [GEQ|H2L]

REM default sorting method Low to High
    Set "SortCompare=LEQ"

REM overide default to High to Low if either of the below given as 3rd Argument
    If /I "%~3" == "GEQ"  Set "SortCompare=GEQ"
    If /I "%~3" == "H2L"  Set "SortCompare=GEQ"
    Set /a "Max=%2 1"

    For /L %%a In (0,1,!Max!)Do (
        If !SortCompare! == LEQ (%= Define Sort offset to match Sort Priority =%
            Set /A "S_Offset=%%a - 1"
        )Else Set /A "S_Offset=%%a"
        For /L %%b IN (0,1,%%a)Do (
            If not %%b==%%a For %%c in (!S_Offset!)Do (%= Expand nested variable for comparison =%
                For /f "tokens=1,2 Delims=;" %%v in ("!%1[%%c]!;!%1[%%b]!")Do IF !%%v! %SortCompare% !%%w! (%= Switch Array Position =%
                        Set "tmpV=!%1[%%c]!"
                        Set "%1[%%c]=!%1[%%b]!"
                        Set "%1[%%b]=!tmpV!"
    )   )   )   )
Exit /B 0

:OutputSorted
    REM output formatting
    Echo(%*
    Echo(Name %\E%[15G Player %\E%[30G Score
    For /f "tokens=2 Delims==" %%G in ('Set Player[')Do (
        For /f "tokens=1,3 Delims=(.)" %%i in ("%%G")Do (
            Echo(%%i %\E%[15G %%j %\E%[30G = !%%G!
        )
    )
    Echo(
Goto:eof

=== GOTO:EOF ===

[/DATA]
500,Bob
390,Thomas
650,Sam
100,Nick
1020,Nick Sr
20,Olivia
115,Sam
[\DATA]
  • Related