VBA Select Case로 5단계·7단계 등급 분류하기

점수 구간을 80 To 89.999처럼 닫힌 숫자로 적으면 89.9999가 어디에도 들어가지 않는 틈이 생깁니다. 이 글에서는 0~100의 소수 점수를 허용하는 연습 계약을 정하고, 큰 하한부터 비교하는 5단계·7단계 등급을 나란히 계산합니다. 문자 라벨은 별도 분류로 다뤄 숫자로 억지 변환하지 않습니다.

점수와 문자 라벨은 다른 입력 계약이다

practice-62.xlsx의 Scores에는 23개 가상 입력이 있습니다. 0~100 범위의 실제 숫자는 소수를 포함해 유효합니다. 빈 셀, 문자 숫자, 음수, 100 초과, 셀 오류, Boolean과 빈 ID는 거부합니다. TextMap은 ID와 문자열 라벨만 받습니다. CSV는 형식이 자동 변환될 수 있으므로 타입 실험은 XLSX를 기준으로 합니다.

점수 구간 5단계 7단계
95~100 A S
90 이상 95 미만 A A
80 이상 90 미만 B B
70 이상 80 미만 C C
60 이상 70 미만 D D
50 이상 60 미만 F E
0 이상 50 미만 F F

이 표는 가상 정책입니다. 소수를 정수로 반올림하지 않습니다. 94.9999는 A/A, 89.9999는 B/B, 85.5도 B/B입니다. 문자로 저장된 85.5는 TYPE이고 숫자 85.5와 구별됩니다.

조건 순서로 구간 틈을 없애기

Select Case에서 여러 조건이 일치하면 첫 번째 일치 조건의 문장만 실행됩니다. 아래 Grade5는 90,80,70,60의 하한을 큰 순서로 비교합니다. 89.9999는 90 이상이 아니고 80 이상이므로 B입니다. 반대로 Case Is >= 60을 맨 앞에 쓰면 95도 D가 되므로, 짧은 조건만 보고 순서를 바꾸면 안 됩니다.

잘못된 반례 Case 80 To 89.999는 89.9999를 포함하지 않습니다. 5단계 분류에서 이를 유일한 B 조건으로 쓰고 마지막 Case Else를 F로 두면 이 점수가 F로 잘못 내려갑니다. 아래 코드는 그 반례를 사용하지 않습니다. 실패를 재현할 때는 별도 연습 함수에서만 비교하고 완성 모듈을 덮어쓰지 마세요.

완전한 분류 모듈과 확인 순서

매크로를 보관할 사본은 Excel 매크로 사용 통합 문서(.xlsm)로 저장합니다. ZIP의 SelectCasePractice.bas를 그 VBA 프로젝트로 가져옵니다. 헤더와 기대값 열은 수정하지 않고 실제 출력 열만 매크로가 쓰게 둡니다.

  1. Scores의 A:B와 기대값 F:H를 선택해 확인한 뒤 GradeScoreExample을 실행합니다. Check에서 처리 23, 유효 15, 거부 8을 확인하고 I열의 행별 대조도 확인합니다.

  2. 5단계 분포 A4/B3/C2/D2/F4, 7단계 분포 S2/A2/B3/C2/D2/E2/F2를 독립 기대값과 비교합니다. 89.9999·85.5·문자 85.5 행을 각각 찾아 등급과 사유를 따로 확인합니다.

  3. TextMap을 선택해 GradeTextExample을 실행합니다. 처리 12, 유효 5, 거부 7이 기대값입니다. VIP와 공백이 있는 gold 는 A, Silver는 B, BRONZE와 basic은 C입니다. 낯선 Goldx는 UNKNOWN이며 빈 값·숫자·Boolean·오류·빈 ID는 각각 사유를 확인합니다.

Attribute VB_Name = "SelectCasePractice"
Option Explicit
Option Compare Binary

Public Sub GradeScoreExample()
    ProcessCases "Scores", 2, True
End Sub

Public Sub GradeTextExample()
    ProcessCases "TextMap", 3, False
End Sub

Private Function ReadScore(ByVal value As Variant, ByRef score As Double) As String
    If IsError(value) Then
        ReadScore = "CELL_ERROR"
        Exit Function
    End If
    If IsEmpty(value) Then
        ReadScore = "EMPTY"
        Exit Function
    End If
    If VarType(value) = vbString Then
        If Len(value) = 0 Then
            ReadScore = "EMPTY"
        Else
            ReadScore = "TYPE"
        End If
        Exit Function
    End If
    Select Case VarType(value)
        Case vbByte, vbInteger, vbLong, vbSingle, vbDouble, vbCurrency, vbDecimal
        Case Else
            ReadScore = "TYPE"
            Exit Function
    End Select
    score = CDbl(value)
    If score < 0 Or score > 100 Then
        ReadScore = "RANGE"
    Else
        ReadScore = "OK"
    End If
End Function

Private Function Grade5(ByVal score As Double) As String
    Select Case score
        Case Is >= 90: Grade5 = "A"
        Case Is >= 80: Grade5 = "B"
        Case Is >= 70: Grade5 = "C"
        Case Is >= 60: Grade5 = "D"
        Case Else: Grade5 = "F"
    End Select
End Function

Private Function Grade7(ByVal score As Double) As String
    Select Case score
        Case Is >= 95: Grade7 = "S"
        Case Is >= 90: Grade7 = "A"
        Case Is >= 80: Grade7 = "B"
        Case Is >= 70: Grade7 = "C"
        Case Is >= 60: Grade7 = "D"
        Case Is >= 50: Grade7 = "E"
        Case Else: Grade7 = "F"
    End Select
End Function

Private Function LabelGrade(ByVal value As Variant, ByRef reason As String) As String
    Dim label As String
    If IsError(value) Then
        reason = "CELL_ERROR"
        Exit Function
    End If
    If IsEmpty(value) Then
        reason = "EMPTY"
        Exit Function
    End If
    If VarType(value) <> vbString Then
        reason = "TYPE"
        Exit Function
    End If
    label = Trim$(UCase$(value))
    reason = "OK"
    Select Case label
        Case "VIP", "GOLD": LabelGrade = "A"
        Case "SILVER": LabelGrade = "B"
        Case "BRONZE", "BASIC": LabelGrade = "C"
        Case "": reason = "EMPTY"
        Case Else: reason = "UNKNOWN"
    End Select
End Function

Private Sub ProcessCases(ByVal sheetName As String, ByVal reportRow As Long, ByVal numericMode As Boolean)
    Const LAST_ALLOWED As Long = 1001
    Dim ws As Worksheet, check As Worksheet
    Dim headers As Variant, data As Variant, results() As Variant
    Dim lastRow As Long, candidate As Long, colNo As Long
    Dim rowCount As Long, outCols As Long, i As Long, validCount As Long
    Dim score As Double, reason As String, grade As String, problem As String
    Dim oldScreen As Boolean, oldEvents As Boolean
    Dim checkReady As Boolean, outputReady As Boolean

    oldScreen = Application.ScreenUpdating
    oldEvents = Application.EnableEvents
    On Error GoTo Failed
    Set check = ThisWorkbook.Worksheets("Check")
    If check.ProtectContents Then
        Err.Raise vbObjectError + 621, , "Practice Check sheet must be writable."
    End If
    checkReady = True
    Application.ScreenUpdating = False
    Application.EnableEvents = False
    check.Range("B" & reportRow & ":F" & reportRow).ClearContents
    check.Cells(reportRow, 2).Value2 = "RUNNING"
    Set ws = ThisWorkbook.Worksheets(sheetName)
    If ws.ProtectContents Then
        Err.Raise vbObjectError + 622, , "Practice input sheet must be writable."
    End If
    If numericMode Then
        headers = Array("id", "score", "grade5", "grade7", "reason", "expected5", "expected7", "expected_reason", "match")
        outCols = 3
    Else
        headers = Array("id", "label", "grade", "reason", "expected_grade", "expected_reason", "match")
        outCols = 2
    End If
    For colNo = 1 To UBound(headers) + 1
        If IsError(ws.Cells(1, colNo).Value2) Then
            Err.Raise vbObjectError + 623, , "Invalid practice header."
        End If
        If CStr(ws.Cells(1, colNo).Value2) <> headers(colNo - 1) Then
            Err.Raise vbObjectError + 624, , "Keep the practice headers unchanged."
        End If
    Next colNo
    outputReady = True
    ws.Range("C2").Resize(LAST_ALLOWED - 1, outCols).ClearContents
    ws.Calculate
    lastRow = 1
    For colNo = 1 To 2
        candidate = ws.Cells(ws.Rows.Count, colNo).End(xlUp).Row
        If candidate > lastRow Then lastRow = candidate
    Next colNo
    If lastRow > LAST_ALLOWED Then
        Err.Raise vbObjectError + 625, , "Practice input is limited to rows 2 through 1001."
    End If
    rowCount = lastRow - 1
    If rowCount = 0 Then GoTo Complete
    data = ws.Range("A2:B" & lastRow).Value2
    ReDim results(1 To rowCount, 1 To outCols)

    For i = 1 To rowCount
        For colNo = 1 To outCols
            results(i, colNo) = ""
        Next colNo
        reason = "NAME"
        If Not IsError(data(i, 1)) Then
            If VarType(data(i, 1)) = vbString Then
                If Len(Trim$(data(i, 1))) > 0 Then
                    If numericMode Then
                        reason = ReadScore(data(i, 2), score)
                        If reason = "OK" Then
                            results(i, 1) = Grade5(score)
                            results(i, 2) = Grade7(score)
                        End If
                    Else
                        grade = LabelGrade(data(i, 2), reason)
                        results(i, 1) = grade
                    End If
                End If
            End If
        End If
        results(i, outCols) = reason
        If reason = "OK" Then validCount = validCount + 1
    Next i
    ws.Range("C2").Resize(rowCount, outCols).Value2 = results

Complete:
    check.Cells(reportRow, 2).Value2 = "DONE"
    check.Cells(reportRow, 3).Value = Now
    check.Cells(reportRow, 4).Value2 = rowCount
    check.Cells(reportRow, 5).Value2 = validCount
    check.Cells(reportRow, 6).Value2 = rowCount - validCount
    ws.Calculate
    check.Calculate
    Application.EnableEvents = oldEvents
    Application.ScreenUpdating = oldScreen
    Exit Sub

Failed:
    problem = Err.Description
    On Error Resume Next
    If outputReady Then ws.Range("C2").Resize(LAST_ALLOWED - 1, outCols).ClearContents
    If checkReady Then
        check.Range("B" & reportRow & ":F" & reportRow).ClearContents
        check.Cells(reportRow, 2).Value2 = "ERROR"
        check.Calculate
    End If
    Application.EnableEvents = oldEvents
    Application.ScreenUpdating = oldScreen
    On Error GoTo 0
    MsgBox "Classification failed: " & problem & vbCrLf & _
        "Do not reuse old output or an old timestamp.", vbExclamation
End Sub

오류를 등급으로 숨기지 않기

VarType은 변수의 하위 형식을 나타내는 정수를 반환합니다. 점수 검사에서는 Boolean도 숫자로 받지 않으며, 문자 숫자를 CDbl로 곧장 바꾸지 않습니다. 유효한 점수만 Grade5·Grade7에 전달하므로 Case Else의 F는 '검사에서 누락된 값 전부'가 아니라 유효한 낮은 점수입니다. TextMap은 ASCII 라벨의 앞뒤 공백을 제거하고 대문자로 맞춘 뒤 정해진 다섯 라벨만 분류합니다. 모든 언어의 공백·대소문자 정규화를 다루는 코드는 아닙니다.

Scores!B22와 TextMap!B12의 =1/0은 CELL_ERROR를 확인하는 의도적 입력입니다. 지워서 조용히 통과시키지 마세요. 배포 파일의 실제 출력은 비어 있고 Check 상태는 NOT RUN입니다. 실행 후 DONE이어도 거부 행이 0이라는 뜻은 아닙니다.

헤더를 바꾸거나 1001행 아래에 입력하면 전체 ERROR로 처리합니다. 틀린 헤더와 입력을 원복하고 새 사본에서 재실행합니다. 예약 출력은 Scores C:E, TextMap C:D의 2~1001행이며 재실행 때 지워집니다. 그 칸에 개인 메모를 넣지 않습니다. 보호된 시트에서는 오류 표시를 쓰는 것 자체가 실패할 수 있으므로 이전 시각이나 등급을 재사용하지 마세요.

검산 결과의 한계

Python 참조 테스트는 23개 점수·12개 라벨의 기대값과 분포, 89.9999의 잘못된 닫힌 구간 반례를 독립 계산합니다. 문자 숫자·Boolean·셀 오류가 XLSX에서 구별되는지도 확인합니다. Excel에서 VBA를 컴파일하거나 실행하지 않았습니다. 실제 Excel에서 가져오기, 컴파일, 쓰기 범위와 Check 갱신을 확인해야 하며 Python의 통과 기록을 VBA 실행 결과로 읽으면 안 됩니다.

입력과 결과 칸을 찾는 기존 그림

그림은 실습 위치를 찾는 보조 자료입니다. 실제 실행 결과는 위 절차로 만든 사본의 값과 상태를 대조합니다.

숫자 점수의 유효성 검사 후 5단계·7단계 등급을 비교하고 문자 점수를 거부하는 설명 도해
숫자 점수의 유효성 검사 후 5단계·7단계 등급을 비교하고 문자 점수를 거부하는 설명 도해. 가상 예제를 설명하는 도해이며 실제 Microsoft Excel 또는 VBA 실행 화면이 아닙니다.
89.9999를 놓치는 닫힌 구간 대신 큰 하한부터 비교해 B를 얻는 설명 도해
89.9999를 놓치는 닫힌 구간 대신 큰 하한부터 비교해 B를 얻는 설명 도해. 가상 예제를 설명하는 도해이며 실제 Microsoft Excel 또는 VBA 실행 화면이 아닙니다.
문자 라벨 12개와 점수 23개의 유효·거부 및 기대 등급 분포를 분리한 설명 도해
문자 라벨 12개와 점수 23개의 유효·거부 및 기대 등급 분포를 분리한 설명 도해. 가상 예제를 설명하는 도해이며 실제 Microsoft Excel 또는 VBA 실행 화면이 아닙니다.

공식 자료

Select Case statement (VBA)

VarType function (Visual Basic for Applications)

Save a macro | Microsoft Support

출처 원문 수집일: S1 2026-09-24; S2 2026-09-24; S3 2026-09-24.

이어서 연습할 글

같은 등급 규칙을 많은 행에 적용할 때 마지막 행과 처리 개수까지 확인하는 방법은 For 루프로 1,000행 분류하기에서 이어집니다.

작성자 소개