hero-image

เผยเทคนิคการเขียนโค้ดแก้โจทย์ “Javascript function” ระดับกลางแบบละเอียดสำหรับ developer ได้ฝึกปรือโดยเฉพาะ

จากกิจกรรม “Dev คนไหนที่คิดว่าเซียนขอเรียนเชิญมาโชว์!” ที่ซันเดย์ได้มอบโจทย์เขียนโค้ด Javascript function เพื่อให้ได้ผลลัพธ์ตามที่กำหนด ได้รับผลตอบรับจากเหล่านักพัฒนากันอย่างมาก เนื่องจากโจทย์นั้นมีความยากท้าทาย และเปิดโอกาสให้พลิกแพลงหาคำตอบได้หลากหลายวิธีการ 

ดังนั้น เพื่อที่จะได้เฉลยโจทย์ได้เคลียร์และชัดเจนที่สุด เราจึงได้เชิญคุณฉัตรชัย กฤชเศรษฐสกุล ตำแหน่ง Technical director ของทีมซันเดย์ ให้มาอธิบายวิธีแก้ไขโจทย์นี้อย่างละเอียด ซึ่งข้อมูลทั้งหมดนี้ก็น่าจะเป็นไกด์สำหรับเหล่า Javascript developer ไม่ว่าจะเป็นมือใหม่หรือมือเก๋าได้เป็นอย่างดี

​​ตัวอย่างโจทย์ Javascript function ที่กำหนดไว้ มีดังนี้

ให้เขียน function ที่แสดงผลลัพธ์ดังภาพ ตาม size ที่รับเข้ามาได้ 

(ความกว้าง ความยาว มีค่าเท่ากัน ดังในตัวอย่างนี้ size = 9) 

$ node draw-diamond.js 9
1 ____0____ 
2 ___000___ 
3 __0_0_0__ 
4 _0__0__0_ 
5 000000000 
6 _0__0__0_ 
7 __0_0_0__ 
8 ___000___ 
9 ____0____ 

สำหรับโจทย์ข้อนี้ถือเป็นโจทย์ในระดับกลาง (intermediate) ที่ใช้ฝึกฝนแนวคิดการเขียน nested loop ให้แสดงผลลัพธ์ได้ถูกต้องตามภาพตัวอย่างโดยมีเป้าหมายสำหรับฝึกฝนการเขียนโค้ดที่ซับซ้อนให้มีความเรียบง่าย (simplify)  สวยงามน่าอ่าน อีกทั้งยังได้ฝึกฝนวิชาคณิตศาสตร์ที่เราเรียนกันในมหาลัยอีกด้วย และที่สำคัญอีกอย่างก็คือเป็นโจทย์ที่ช่วยกระตุ้นให้ลองคิดแก้ปัญหาเฉพาะหน้า เมื่อต้องเจอกับความต้องการ (requirement) ที่ไม่ชัดเจน ที่มักเกิดขึ้นได้เสมอในการทำงานจริง โดยเฉพาะการทำงานในตำแหน่ง developer 

หากท่านผู้อ่านเป็น developer ที่ไม่ได้เขียนภาษา Javascript ก็อยากชวนให้ลองนำโจทย์ข้อนี้ ไปลองเขียนในภาษาที่คุณถนัดได้เช่นกัน

แนวทางแก้โจทย์ Javascript function (Step 1)

อันดับแรก ให้ลองจินตนาการแยกรูปเพชรออกเป็นเส้นๆ ตามภาพประกอบด้านล่างกันดูก่อน

จะเห็นว่าเราสามารถแบ่งเส้นออกเป็น 4 ชุด ซึ่งเมื่อคิดได้ตามนี้แล้ว วิธีการเขียนโค้ดก็เพียงแค่วน loop ไปทีละช่องตามด้านกว้างและด้านสูง เมื่อถึงช่องที่ต้องวาดเส้น (จุดแดง) ก็แสดงตัวอักษร ‘0’ ตามที่โจทย์กำหนด มาลองดูวิธีกัน

  • สมมติรับค่าความกว้าง = ยาว = 9 ช่อง
    • size = 9
  • เริ่มจากตรวจสอบขนาดว่าเป็นเลขคึ่หรือไม่
    • odd = size % 2
    • odd = 1
  • คำนวณหา maxIndex ที่เป็นไปได้ เพื่อไม่ให้วาดเลยออกนอกขนาดที่กำหนด
    • maxIndex = size – odd
    • maxIndex = 8
  • คำนวณหาตำแหน่ง index ตรงกลางรูป โดยลบด้วย odd เพื่อให้ค่าเป็นเลขจำนวนเต็ม
    • mid = maxIndex / 2
    • mid = 4
  • เขียน loop สองชั้น
    • ชั้นแรกให้มองเป็นแกน y / ชั้นที่สองให้มองเป็นแกน x
    • เช็คตำแหน่ง x, y ปัจจุบัน ว่าอยู่ในจุดแดงตามภาพหรือไม่ ถ้าใช่ให้แสดง ‘0’ ถ้าไม่ใช่ให้แสดง ‘_’
    • เอาเงื่อนไขทั้งหมดมาเชื่อมกันด้วย || (or) ให้อยู่ใน condition เดียวกันไปได้เลย
function draw(size) {
  const odd = size % 2;
  const maxIndex = size - odd;
  const midIndex = maxIndex / 2;

  for (let y = 0; y < size; y++) {
    let line = '';
    for (let x = 0; x < size; x++) {
      if (
        y == midIndex                       // mid-horizontal line
        || x == midIndex                    // mid-vertical line
        || (x + y) % maxIndex == midIndex   // Q1,Q4 diagonal line
        || Math.abs(x - y) == midIndex      // Q2,Q3 diagonal line
      ) {
        line += '0';
      } else {
        line += '_';
      }
    }
    console.log(line);
  }
}

เพียงเท่านี้ก็วาดรูปเพชร ด้วย size = 9 ได้แล้ว แต่…

แนวทางแก้โจทย์ Javascript function (Step 2)

  • แต่จะสังเกตว่าในโจทย์นี้มีจุดที่ “ไม่ชัดเจน” อยู่จุดนึง นั่นก็คือโจทย์ไม่ได้บอกว่าถ้ารับขนาดเข้ามาเป็นเลขคู่ จะให้แสดงผลเป็นอย่างไร?
  • ที่จริงแล้วโจทย์นี้มีความตั้งใจที่จะบอกโจทย์ให้ไม่มีความชัดเจนนั่นเอง เพราะเรามีความเชื่ออย่างนึงว่า ในสถานการณ์ทำงานจริง คนทำงานจะไม่มีทางได้ requirement ชัดเจน 100% จากลูกค้าแน่ๆ ดังนั้นเมื่อเราเจอ requirement ไม่ชัดเจนแบบนี้ สิ่งที่ควรทำก็คือ
    • หาทางเช็กความต้องการให้เคลียร์ก่อนลงมือทำ
    • แต่ถ้าไม่สามารถเช็กความต้องการได้จริงๆ แต่จำเป็นต้องเริ่มทำงานแล้ว ก็ต้องลองทำไปด้วยแนวทางที่ใกล้เคียง สอดคล้องกับความต้องการของลูกค้าให้ได้มากที่สุด
  • ดังนั้น รูปเพชรควรจะแสดงออกมาหน้าตาประมาณนี้
1  ____00____
2  ___0000___
3  __0_00_0__
4  _0__00__0_
5  0000000000
6  0000000000
7  _0__00__0_
8  __0_00_0__
9  ___0000___
10 ____00____
  • แค่ปรับโค้ดเพิ่มไม่มากเท่าไร ก็จะสามารถทำงานได้แล้ว โดยเอาค่า odd มาใช้ร่วมในการเช็คตำแหน่งด้วย ดังนี้
function draw(size) {
 const odd = size % 2;
 const maxIndex = size - odd;
 const midIndex = maxIndex / 2 - !odd;
 
 for (let y = 0; y < size; y++) {
   let line = '';
   for (let x = 0; x < size; x++) {
     if (
       y == midIndex || y == midIndex + !odd      // mid-horizontal line
       || x == midIndex || x == midIndex + !odd   // mid-vertical line
       || (x + y) % maxIndex == midIndex          // Q1,Q4 diagonal line
       || Math.abs(x - y) == midIndex + !odd      // Q2,Q3 diagonal line
     ) {
       line += '0';
     } else {
       line += '_';
     }
   }
   console.log(line);
 }
}
  • ตรงนี้ได้มีการใช้ความสามารถเฉพาะตัวของ JavaScript เล็กน้อย (ไม่ควรเลียนแบบเอาไปเขียนกับภาษาอื่นที่เป็น strong type) โดยเมื่อเราใช้ unary not กับตัวเลข จะได้ว่า !1 → 0 และ !0 → 1 แล้วเราก็สามารถนำค่าไปใช้เช็คต่อได้ง่ายๆ เลย

แนวทางแก้โจทย์ที่ Javascript function ที่ “Perfect” ที่สุด

สำหรับเงื่อนไขการแก้โจทย์ที่ “Perfect” ที่สุดความหมายก็คือ

  1. ต้องรันแล้วได้ผลลัพธ์ถูกต้องทุกกรณี ในที่นี้รวมถึงเลขบรรทัด มีเว้นช่องว่างก่อนแสดงคำตอบแต่ละบรรทัด และแม้ว่า input จะเป็นเลขคู่ก็ต้องทำงานได้ใกล้เคียงกับ requirement มากที่สุดเช่นกัน
  1. โค้ดที่เขียนออกมาแล้วต้องมีความกระชับ เข้าใจง่าย ไม่ซับซ้อน เมื่อส่งโค้ดต่อให้คนอื่นแล้ว คนรับช่วงต่อจะไม่บ่นว่า “เขียนอะไรมาครับเนี่ย” แม้จะไม่ได้เขียน comment เลยก็ตาม
  1. คำนึงถึง Time / Space Complexity ทำงานรวดเร็ว ใช้ memory น้อย
  1. หรือมีการใช้เทคนิคที่ “คาดไม่ถึง” แต่เหมาะสมและมีประสิทธิภาพสูง

ขออนุญาตนำแนวทางโค้ดที่ผมชอบที่สุดของคุณ Atdhasiri Sangchan มาวิเคราะห์ให้ฟังว่าทำไมถึง “Perfect” ในความคิดของผมนะครับ

function draw(size) {
 let mid = (size + 1) / 2
 for (let row = 1; row <= size; row++) {
   process.stdout.write((" " + (row) + " ").slice(-3)) //padding left if one digit
   let distRowToMid = Math.abs(row - mid) //distance from current row to middle
   for (let col = 1; col <= size; col++) {
     let distColToMid = Math.abs(col - mid) //distance from current column to middle
     //compare with 0.5 to handle even number
     if (distRowToMid + distColToMid == Math.ceil(mid) - 1 || distRowToMid <= 0.5 || distColToMid <= 0.5) {
       process.stdout.write("0")
     } else {
       process.stdout.write("_")
     }
   }
   console.log() //new line
 }
}
  • ขอ Highlight ที่บรรทัดนี้เลยครับ เท่จริงๆ distRowToMid + distColToMid == Math.ceil(mid) - 1 ถือเป็นการเช็คตำแหน่งที่สั้น กระชับ “คาดไม่ถึง” และทำงานได้ถูกต้องครับ แค่บรรทัดนี้บรรทัดเดียวก็สามารถวาดรูปสี่เหลี่ยมข้าวหลามตัดออกมาได้แล้ว
  • การวาดเส้นกลางรูปในแนวตั้งกับแนวนอน ก็ “คาดไม่ถึง” ด้วยเช่นกัน ใช้การเช็คด้วย condition <= 0.5 กล่าวคือ ถ้า size เป็นเลขคี่ หารแล้วเหลือเศษทศนิยม .5 ก็ยังทำงานได้ถูกตามที่กล่าวไปข้างต้นเช่นกัน โดยไม่ต้องแยกออกเป็น 2 conditions
  • วิธีการแสดงผลบน console ก็คิดมาดีเช่นกัน ไม่ใช่การประกอบ string แล้ว print ออกมาทีละบรรทัดอย่างที่หลายๆ คน (รวมถึงผมด้วย) ทำ แต่ใช้วิธีปล่อยออกไปที่ stdout ทีละตัวเลย ทำให้ไม่ต้องกังวลเรื่อง memory space คือไม่ได้ใช้พื้นที่ของ memory เพื่อช่วยแสดงผลลัพธ์เลยแม้แต่นิดเดียว เยี่ยมยอดครับ!
  • แต่ๆๆๆ ก็ยังมีจุดพลาดเล็กน้อยนะครับ.. คือ ถ้าสั่ง draw(100) การแสดงเลขบรรทัดที่ 100 ตัวเลข 1 จะหายไป 
  • ซึ่งตรงนี้ผมขอนำวิธีที่ประยุกต์จากแนวทางของคุณ Kamonnop Ti Arunrat มาเล่าสู่กันฟังครับ วิธีการนี้ “คาดไม่ถึง” อีกเช่นกัน
    let maxDigit = Math.floor(Math.log10(size)) + 1
  • คำสั่ง Math.log10(size) จะทำให้เรารู้ว่า เลขบรรทัดใช้พื้นที่กี่ตัวอักษร + บวกช่องว่างไปอีก 1 ช่อง จากนั้นเมื่อขึ้นบรรทัดใหม่ทุกครั้ง ให้แสดงเลขบรรทัดดังนี้ row.toString().padEnd(maxDigit) ก็จะได้เลขบรรทัดที่เว้น space เท่ากันเสมอทุกบรรทัดแล้ว

เราหวังว่าโจทย์ข้อนี้จะสามารถสร้างความสนุกสนาน และได้ลองท้าทายความสามารถของเพื่อนๆ พี่ๆ น้องๆ developer ทุกท่านได้เป็นอย่างดี และก็ขอแสดงความยินดีกับผู้ที่ได้รับรางวัล “Keychron K3” Mechanical keyboard ทั้งสองท่านด้วย หากมีกิจกรรมเจ๋งๆ แบบนี้ครั้งหน้าเรามาร่วมสนุกกันใหม่ และอย่าลืมกด Like FB fanpage Sunday Thailand เพื่อที่จะไม่พลาดข่าวสาร และกิจกรรมสนุกๆแบบนี้เป็นประจำ

สุดท้ายนี้ก่อนจะจากกันไป ขอบอกไว้ซักนิดว่าตอนนี้ซันเดย์กำลังเปิดรับสมัครเพื่อนร่วมทีมหลายตำแหน่ง โดยเฉพาะถ้า developer คนไหนที่อยากจะเข้ามาเป็นส่วนหนึ่งของบริษัท Insurtech ที่จะเปลี่ยนโฉมหน้าวงการประกัน รวมถึงคาดหวังที่จะได้ท้าทายความสามารถของตนเองมากกว่านี้ล่ะก็ เช็กตำแหน่งที่เปิดรับที่นี่ได้เลย