SFC Grade Util Script

Author

gentam

Date
2017-09-09
Category

snippet

Tag

util, keio, sfc, college

Updated

2018-09-05

Changelog:

Contents

慶應大学では成績をWebから確認することができるがやや不便なところがある:

という訳でこれらをやってくれるスクリプトを書いた.SFC生用に4年への進級条件と卒業 条件も表示される.自分で使う機会はあまり残されていないので誰かの役に立つと嬉しい.

Web版

  1. SFS → 学事Webシステム → 学業成績表閲覧

  2. 表の部分だけを選択("科目名称"の左をクリック→スクロールしてShiftキーを押しなが ら"以上"の右をクリック)してコピーする

  3. ここにペーストする:

  4. GPAの計算に使う値を選択: (詳細は GPA計算方法 参照)

別の統計方法が必要な場合は,コンソールからグローバル変数 G でパースされたメ インのオブジェクトにアクセスできるので,計算用にスクリプトを書くことができる. あるいは# ローカル版 を編集して実行する.

進級/卒業要件は KEIO SFC GUIDE 2018 p42参照.なお07学則には対応していません.

GPA計算方法

\(\frac{(Grade Points \times 単位数) の総和}{総履修単位数}\)

標準

成績証明書における成績評価について <http://www.gakuji.keio.ac.jp/academic/shoumei/grading_system.html>

S

A

B

C

D

~2016

4

3

2

0

2017~

4

3

2

1

0

交換留学

出願資格 <http://www.ic.keio.ac.jp/keio_student/exchange/qualifications.html>

S

A

B

C

D

~2016

5

4

3

0

2017~

5

4.5

4

3

0

奨学金(JASSO用成績係数)

2018年度日本学生支援機構(JASSO)海外留学支援制度(協定派遣)および慶應義塾大学交換留学生(派遣)奨学金 募集要項 <http://www.ic.keio.ac.jp/keio_student/scholarship/2018jasso_1.2.ad.html>

S

A

B

C

D

~2016

3

2

1

0

2017~

3

3

2

1

0

ローカル版

最初に書いたNodeでファイルを読んで実行するバージョン. Gist <https://gist.github.com/gentam/402b64a9648a90b385c23d23e02cbe1b>

  1. curl -o gradeUtil.js https://gist.githubusercontent.com/gentam/402b64a9648a90b385c23d23e02cbe1b/raw/1397844b6274cc186a389b322fb4f0f3b5fc7ab5/gradeUtil.js

  2. SFS > 学事Webシステム > 学業成績表を表示

  3. 表の部分だけを選択してコピー

  4. macOSの場合: pbpaste > record.txt

  5. node gradeUtil.js record.txt

const fs   = require('fs')
const path = require('path')

const PASS = 'pass'
const FAIL = 'fail'
const EXP = 'expected'

// 標準 <http://www.gakuji.keio.ac.jp/academic/shoumei/grading_system.html>
const gradeTbl16 = {         'A': 4, 'B': 3, 'C': 2, 'D': 0, '★': FAIL, 'P': PASS, 'F': FAIL, 'G': PASS, '?': EXP, '-': EXP}
const gradeTbl17 = {'S': 4, 'A': 3, 'B': 2, 'C': 1, 'D': 0,            'P': PASS, 'F': FAIL, 'G': PASS, '?': EXP, '-': EXP}

// // 交換留学用 <http://www.ic.keio.ac.jp/keio_student/exchange/qualifications.html>
// const gradeTbl16 = {         'A': 5,   'B': 4, 'C': 3, 'D': 0, '★': FAIL, 'P': PASS, 'F': FAIL, 'G': PASS, '?': EXP, '-': EXP}
// const gradeTbl17 = {'S': 5, 'A': 4.5, 'B': 4, 'C': 3, 'D': 0,            'P': PASS, 'F': FAIL, 'G': PASS, '?': EXP, '-': EXP}

// // 奨学金用 (JASSO用成績係数) <http://www.ic.keio.ac.jp/keio_student/scholarship/2018jasso_1.2.ad.html>
// const gradeTbl16 = {         'A': 3, 'B': 2, 'C': 1, 'D': 0, '★': FAIL, 'P': PASS, 'F': FAIL, 'G': PASS, '?': EXP, '-': EXP}
// const gradeTbl17 = {'S': 3, 'A': 3, 'B': 2, 'C': 1, 'D': 0,            'P': PASS, 'F': FAIL, 'G': PASS, '?': EXP, '-': EXP}

const indexTbl = {
  className: 0,
  profName:  1,
  grade:     2,
  credit:    3,
  makeup:    4,
  year:      5,
  term:      6,
  when:      7,
}

;(function () {
  const argv = process.argv
  if (argv.length !== 3) {
    console.log(`usage: node ${path.basename(argv[1])} fileName`)
    return
  }
  readFile(argv[2])
    .then(read   => parseFile(read))
    .then(parsed => calcStat(parsed))
    .then(res    => formatInfo(res))
    .catch(err   => print(err))
})()

function readFile (filePath) {
  return new Promise((resolve, reject) => {
    fs.readFile(filePath, 'utf8', (err, data) => {
      if (err) reject(err)
      resolve(data)
    })
  })
}

// parseFile :: String fileData -> {'header': [String header ...],
//                                  'List': {String category: [[String className ...] ...] ...}}
function parseFile (fileData) {
  return new Promise ((resolve, reject) => {
    let Parsed = {}
    let table = fileData.split('\n').map(line =>
      line.split('\t').map(e => e.trim()))
    Parsed.header = table.shift()
    Parsed.List = {}

    let category = ''
    table.forEach(row => {
      if (row.length === 2) {
        category = row[0].slice(12)
        Parsed.List[category] = []
      } else if (row.length === 8) {
        Parsed.List[category].push(row)
      }
    })
    resolve(Parsed)
  })
}

// calcStat :: Object Parsed -> +{'Stat': {sum_credit ...},
//                                'List': {'Stat': {Number credit, Number grade ...} ...}
function calcStat (Parsed) {
  Parsed.Stat = {}
  let Cat     = {}
  let Type    = {}

  let sum_credit       = 0
  let sum_grade        = 0
  let sum_failed       = 0
  let sum_pending      = 0
  let sum_creditExtra  = 0
  let sum_gradeExtra   = 0
  let sum_failedExtra  = 0
  let sum_pendingExtra = 0
  let sum_deduct       = 0 // deduct PASS/FAIL credits from GPA calculation

  Object.keys(Parsed.List).forEach(category => {
    let credit  = 0
    let grade   = 0
    let failed  = 0
    let pending = 0
    let deduct  = 0

    Parsed.List[category].forEach(row => {
      let gradeTbl = (Number(row[indexTbl.year]) < 2017) ? gradeTbl16 : gradeTbl17

      let c = Number(row[indexTbl.credit])
      let g = gradeTbl[row[indexTbl.grade]]
      switch (g) {
        case PASS: credit += c
                   deduct += c; break
        case FAIL: failed += c
                   deduct += c; break
        case EXP: pending += c; break
        default:
          grade += g * c
          if (g === 0) failed += c
          else         credit += c
      }
    })

    // 自由科目の単位は卒業単位には含まれない
    if (category.includes('自由科目')) {
      sum_creditExtra  += credit
      sum_gradeExtra   += grade
      sum_failedExtra  += failed
      sum_pendingExtra += pending
    } else {
      sum_credit  += credit
      sum_grade   += grade
      sum_failed  += failed
      sum_pending += pending
      sum_deduct  += deduct
    }

    Cat[category] = {
      credit,
      grade,
      failed,
      pending,
      gpa: grade / (credit + failed - deduct),
    }
  })

  Type = gatherType(Cat)

  Parsed.Stat = {
    sum_credit,
    sum_grade,
    sum_failed,
    sum_pending,
    sum_creditExtra,
    sum_gradeExtra,
    sum_failedExtra,
    sum_pendingExtra,

    sum_creditWithExtra: sum_credit + sum_creditExtra,
    gpa: sum_grade / (sum_credit + sum_failed - sum_deduct),
    gpaWithExtra: (sum_grade + sum_gradeExtra) / (sum_credit + sum_failed + sum_creditExtra + sum_failedExtra - sum_deduct),
    Type,
    Cat,
  }

  return Parsed
}

// gatherType :: Object Categories -> {String type: {joinedStat} ...}
function gatherType (Cat) {
  Type = {}
  Object.keys(Cat).forEach(category => {
    let prefix = category.split(' ')[0]
    if (Type[prefix]) Type[prefix] = joinObj(Type[prefix], Cat[category])
    else              Type[prefix] = Cat[category]
  })
  Object.keys(Type).forEach(t => {
    Type[t].gpa = Type[t].grade / Type[t].credit
  })
  return Type
}

function joinObj (A, B) {
  R = {}
  Object.keys(A).forEach(keyA => {
    Object.keys(B).forEach(keyB => {
      if (keyA === keyB)
        R[keyA] = A[keyA] + B[keyB]
    })
  })
  return R
}

// formatInfo :: Object Record -> IO
function formatInfo (Record) {
  // http://www.gakuji.keio.ac.jp/sfc/pe/3946mc0000022u8x-att/3946mc0000022vc8.pdf
  const req_senior_basic      = 30
  const req_graduate_advanced = 30
  const req_graduate_total    = 124

  let S = Record.Stat
  info = `現在の取得単位: ${S.sum_credit}
落とした単位: ${S.sum_failed}
GPA: ${S.gpa.toFixed(2)}
取得予定の単位: ${S.sum_pending}

--------------------------
4年進級条件:
  基盤科目: ${S.Type['基盤科目'] ?
      S.Type['基盤科目'].credit : 0}/${req_senior_basic}
  体育2・3: ${S.Cat['基盤科目 ウェルネス科目 体育2・3'] ?
      S.Cat['基盤科目 ウェルネス科目 体育2・3'].credit : 0}/2
  研究会: ${S.Cat['研究プロジェクト科目 研究会'] ?
      S.Cat['研究プロジェクト科目 研究会'].credit : 0}/2

卒業条件:
  先端科目: ${S.Type['先端科目'] ?
      S.Type['先端科目'].credit : 0}/${req_graduate_advanced}
  合計: ${S.sum_credit}/${req_graduate_total} ${(S.sum_credit < req_graduate_total) ?
      ['(あと', req_graduate_total - S.sum_credit, '単位)'].join('') : ''}

--------------------------
自由科目の情報:
  取得単位: ${S.sum_creditExtra} (合計${S.sum_creditWithExtra})
  落とした単位: ${S.sum_failedExtra}
  自由科目込みのGPA: ${S.gpaWithExtra.toFixed(2)}
  取得予定の単位: ${S.sum_pendingExtra}
`

  console.log(info)
}

function print (data) {
  console.log(JSON.stringify(data, null, 4))
}