Jitrak Blog

Switch true

Switch true ลาก่อนบุกป่าฝ่าดง If

11 Jul 2023 13:00

Written by: Yosapol Jitrak

Tags:

Condition

ปกติเวลาเราเขียนเงื่อนไขต่าง ๆ เรามักจะใช้ if-else กันเป็นปกติ ยกตัวอย่างเป็นโปรแกรมตัดเกรดนักศึกษา

enum Grade {
  A = 'A',
  B = 'B',
  C = 'C',
  D = 'D',
  F = 'F',
}

interface Student {
  id: number;
  name: string;
  score: number;
  grade?: string;
}

const students: Student[] = [
  { id: 1, name: 'Alice', score: 85 },
  { id: 2, name: 'Bob', score: 72 },
  { id: 3, name: 'Charlie', score: 56 },
  { id: 4, name: 'David', score: 65 },
  { id: 5, name: 'Eve', score: 49 },
];

students.forEach((student) => {
  if (student.score >= 80) {
    student.grade = Grade.A;
  } else if (student.score >= 70) {
    student.grade = Grade.B;
  } else if (student.score >= 60) {
    student.grade = Grade.C;
  } else if (student.score >= 50) {
    student.grade = Grade.D;
  } else {
    student.grade = Grade.F;
  }
});

console.table(students);

ผลลัพท์ student-grade-result

คราวนี้เราจะมีความรู้สึกว่า มันควรจะเขียนเป็น Switch case ได้หรือเปล่านะ สำหรับโจทย์นี้

students.forEach((student) => {
  switch (Math.floor(student.score / 10)) {
    case 10:
    case 9:
    case 8:
      student.grade = Grade.A;
      break;
    case 7:
      student.grade = Grade.B;
      break;
    case 6:
      student.grade = Grade.C;
      break;
    case 5:
      student.grade = Grade.D;
      break;
    default:
      student.grade = Grade.F;
      break;
  }
});
console.table(students);

ก็ดูโอเคขึ้น จะมีขัดใจตรง case 10, 9 และ 8
เรามาลองดูตัวอย่าง Switch true กันครับ

students.forEach((student) => {
  switch (true) {
    case student.score >= 80:
      student.grade = Grade.A;
      break;
    case student.score >= 70:
      student.grade = Grade.B;
      break;
    case student.score >= 60:
      student.grade = Grade.C;
      break;
    case student.score >= 50:
      student.grade = Grade.D;
      break;
    default:
      student.grade = Grade.F;
      break;
  }
});
console.table(students);

ท่านี้ยังไงก็สวยกว่าการใช้ if-else และไม่ต้องเขียน case 10, 9 ที่ไม่จำเป็น ในกรณีที่เป็น switch case ธรรมดา รวมถึงมีความชัดเจนในตัวที่มากกว่า

ถ้าโจทย์ของจริง มันอาจจะไม่ได้ง่ายแบบนี้ หลายครั้งเราไม่สามารถใช้เงื่อนไขเดียวในการเช็คได้แบบนี้ ต้องมี and หรือ or ด้วย
ยกตัวอย่างเป็นโจทย์ชนะทางแพ้ทางของเกม

graph LR
    water --> |2| fire
    fire --> |2| grass
    grass --> |2| earth
    earth --> |2| electric
    electric --> |2| water
    dark --> |1| light
    dark --> |1.2| water
    dark --> |1.2| fire
    dark --> |1.2| grass
    dark --> |1.2| earth
    dark --> |1.2| electric
    light --> |3| dark
    light --> |0.9| water
    light --> |0.9| fire
    light --> |0.9| grass
    light --> |0.9| earth
    light --> |0.9| electric

ยกตัวอย่างข้อมูลแถวแรก คือ น้ำชนะไฟ Damage จะคูณ 2 การเขียนด้วย if-else ทำได้แน่นอน แต่มันจะรกมาก

export const allElements = ['normal', 'water', 'fire', 'grass', 'earth', 'electric', 'light', 'dark'] as const;

export type ElementType = (typeof allElements)[number];

export type CalculateMultiplierInput = [ElementType, ElementType];

// water -> fire -> grass -> earth -> electric -> water | 2
// dark -> every | 1.2
// light -> dark | 3
// light -> not dark | 0.9
export const calculateDamageMultiplier = (input: CalculateMultiplierInput) => {
  let multiplier: number;
  if (input[0] === 'water' && input[1] === 'fire') {
    multiplier = 2;
  } else if (input[0] === 'fire' && input[1] === 'grass') {
    multiplier = 2;
  } else if (input[0] === 'grass' && input[1] === 'earth') {
    multiplier = 2;
  } else if (input[0] === 'earth' && input[1] === 'electric') {
    multiplier = 2;
  } else if (input[0] === 'electric' && input[1] === 'water') {
    multiplier = 2;
  } else if (input[0] === 'dark') {
    multiplier = 1.2;
  } else if (input[0] === 'light' && input[1] === 'dark') {
    multiplier = 3;
  } else if (input[0] === 'light') {
    multiplier = 0.9;
  } else {
    multiplier = 1;
  }

  return multiplier;
};

คราวนี้ลองมาดู Version switch true กันบ้างครับ

export const calculateDamageMultiplier = (input: CalculateMultiplierInput) => {
  let multiplier: number;
  switch (true) {
    case input[0] === 'water' && input[1] === 'fire':
      multiplier = 2;
      break;
    case input[0] === 'fire' && input[1] === 'grass':
      multiplier = 2;
      break;
    case input[0] === 'grass' && input[1] === 'earth':
      multiplier = 2;
      break;
    case input[0] === 'earth' && input[1] === 'electric':
      multiplier = 2;
      break;
    case input[0] === 'electric' && input[1] === 'water':
      multiplier = 2;
      break;
    case input[0] === 'dark':
      multiplier = 1.2;
      break;
    case input[0] === 'light' && input[1] === 'dark':
      multiplier = 3;
      break;
    case input[0] === 'light':
      multiplier = 0.9;
      break;
    default:
      multiplier = 1;
      break;
  }

  return multiplier;
};

จาก Code ชุดนี้ จะเห็นได้ว่า Code มีความอ่านง่ายมากกว่า if-else แบบก่อนหน้านี้ และช่วยลด Human error ได้

สำคัญ อย่าลืมเขียน Unit test ด้วยนะครับ

import { calculateDamageMultiplier, CalculateMultiplierInput, ElementType } from './element';

describe('calculateDamageMultiplier', () => {
  test.each<CalculateMultiplierInput>([
    ['normal', 'normal'],
    ['fire', 'fire'],
    ['fire', 'electric'],
  ])(
    'should be 1 when attackerElement and defenderElement is not match any case.',
    (attackerElement, defenderElement) => {
      // Arrange
      const input: CalculateMultiplierInput = [attackerElement, defenderElement];
      const expectedDamageMultiplier = 1;

      // Act
      const damageMultiplier = calculateDamageMultiplier(input);

      // Assert
      expect(damageMultiplier).toEqual(expectedDamageMultiplier);
    },
  );

  test('should be 2 when attackerElement is water and defenderElement is fire.', () => {
    // Arrange
    const attackerElement: ElementType = 'water';
    const defenderElement: ElementType = 'fire';
    const input: CalculateMultiplierInput = [attackerElement, defenderElement];
    const expectedDamageMultiplier = 2;

    // Act
    const damageMultiplier = calculateDamageMultiplier(input);

    // Assert
    expect(damageMultiplier).toEqual(expectedDamageMultiplier);
  });

  test('should be 2 when attackerElement is fire and defenderElement is grass.', () => {
    // Arrange
    const attackerElement: ElementType = 'fire';
    const defenderElement: ElementType = 'grass';
    const input: CalculateMultiplierInput = [attackerElement, defenderElement];
    const expectedDamageMultiplier = 2;

    // Act
    const damageMultiplier = calculateDamageMultiplier(input);

    // Assert
    expect(damageMultiplier).toEqual(expectedDamageMultiplier);
  });

  test('should be 2 when attackerElement is grass and defenderElement is earth.', () => {
    // Arrange
    const attackerElement: ElementType = 'grass';
    const defenderElement: ElementType = 'earth';
    const input: CalculateMultiplierInput = [attackerElement, defenderElement];
    const expectedDamageMultiplier = 2;

    // Act
    const damageMultiplier = calculateDamageMultiplier(input);

    // Assert
    expect(damageMultiplier).toEqual(expectedDamageMultiplier);
  });

  test('should be 2 when attackerElement is earth and defenderElement is electric.', () => {
    // Arrange
    const attackerElement: ElementType = 'earth';
    const defenderElement: ElementType = 'electric';
    const input: CalculateMultiplierInput = [attackerElement, defenderElement];
    const expectedDamageMultiplier = 2;

    // Act
    const damageMultiplier = calculateDamageMultiplier(input);

    // Assert
    expect(damageMultiplier).toEqual(expectedDamageMultiplier);
  });

  test('should be 2 when attackerElement is electric and defenderElement is water.', () => {
    // Arrange
    const attackerElement: ElementType = 'electric';
    const defenderElement: ElementType = 'water';
    const input: CalculateMultiplierInput = [attackerElement, defenderElement];
    const expectedDamageMultiplier = 2;

    // Act
    const damageMultiplier = calculateDamageMultiplier(input);

    // Assert
    expect(damageMultiplier).toEqual(expectedDamageMultiplier);
  });

  test.each<ElementType>(['normal', 'water', 'fire', 'grass', 'electric'])(
    'should be 1.2 when attackerElement is dark and defenderElement is basic element.',
    (defenderElement) => {
      // Arrange
      const attackerElement: ElementType = 'dark';
      const input: CalculateMultiplierInput = [attackerElement, defenderElement];
      const expectedDamageMultiplier = 1.2;

      // Act
      const damageMultiplier = calculateDamageMultiplier(input);

      // Assert
      expect(damageMultiplier).toEqual(expectedDamageMultiplier);
    },
  );

  test('should be 3 when attackerElement is light and defenderElement is dark.', () => {
    // Arrange
    const attackerElement: ElementType = 'light';
    const defenderElement: ElementType = 'dark';
    const input: CalculateMultiplierInput = [attackerElement, defenderElement];
    const expectedDamageMultiplier = 3;

    // Act
    const damageMultiplier = calculateDamageMultiplier(input);

    // Assert
    expect(damageMultiplier).toEqual(expectedDamageMultiplier);
  });

  test.each<ElementType>(['normal', 'water', 'fire', 'grass', 'electric'])(
    'should be 0.9 when attackerElement is light and defenderElement is basic element.',
    (defenderElement) => {
      // Arrange
      const attackerElement: ElementType = 'light';
      const input: CalculateMultiplierInput = [attackerElement, defenderElement];
      const expectedDamageMultiplier = 0.9;

      // Act
      const damageMultiplier = calculateDamageMultiplier(input);

      // Assert
      expect(damageMultiplier).toEqual(expectedDamageMultiplier);
    },
  );
});

จริง ๆ เอา Code ชุดนี้ผมก็ยังไม่พอใจหรอกนะครับ
มันมี Tool ที่จะมาช่วยให้มันเขียนได้สะอาดกว่านี้ได้ ซึ่งจะมาพูดถึงกันในบทความถัดไปครับ ts-pattern

ลองดูตัวอย่างที่กล่าวถึง Switch true ได้จาก:
Using the Switch(true) Pattern in JavaScript
functional-bowling-ts