Skip to content

Instantly share code, notes, and snippets.

@Jessidhia
Last active May 27, 2022 07:32
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Jessidhia/dc117754ec668421eadb60532646a0a7 to your computer and use it in GitHub Desktop.
Save Jessidhia/dc117754ec668421eadb60532646a0a7 to your computer and use it in GitHub Desktop.
// @ts-check
// ==UserScript==
// @name Export Suica transactions to CSV
// @namespace http://tampermonkey.net/
// @version 0.2
// @icon https://www.jreast.co.jp/favicon.ico
// @description Export Suica transactions to CSV
// @updateURL https://gist.github.com/Jessidhia/dc117754ec668421eadb60532646a0a7/raw/suica-script.user.js
// @author Jessidhia
// @include https://www.mobilesuica.com/iq/ir/SuicaDisp.aspx*
// @grant none
// ==/UserScript==
;(function () {
'use strict'
// Could be "JR East", but the card can be used anywhere so use a generic name.
// It's in a const here so it can be changed.
const TransportationPayee = 'Suica Transport'
const buttonRow = document.querySelector(
'.historyBox .grybg01 tr .rightElm'
)
if (!buttonRow || !buttonRow.children[0]) {
return
}
const button = document.createElement('button')
button.type = 'button'
button.className = 'list_title'
button.style.marginRight = '0.5em'
button.textContent = 'Export to CSV'
const titleRow = document.querySelector('.historyTable tr.NoLine')
if (!titleRow) {
return
}
const titleData = Array.from(titleRow.children, td => td.textContent)
// ["月/日","種別","利用場所","種別","利用場所","残額","差額"]
if (
JSON.stringify(titleData) !==
`["","月日","種別","利用場所","種別","利用場所","残高","入金・利用額"]`
) {
alert('Suica table format changed; aborting')
return
}
buttonRow.insertBefore(button, buttonRow.children[0])
button.addEventListener('click', () => {
const rawDataRows = Array.from(
// .NoLine is the titleRow we just validated before starting
// :last-child is just an initial balance value and not useful to us
document.querySelectorAll(
'.historyTable tr:not(.NoLine):not(:last-child)'
),
tr =>
Array.from(tr.children, td =>
// Replace full-width spaces with half-width, trim the excess
// prettier-ignore
/** @type {string} */ (td.textContent).replace(/ /g, ' ').trim()
)
)
const csvTitleRow = ['Date', 'Payee', 'Memo', 'Amount']
const dataRows = rawDataRows.map(
([, date, type, location, kind, exitLocation, , amountStr]) => {
const amount = amountStr.replace(/,/g, '')
if (!location) {
// We just know it was a transation but no idea from where or what
return [date, '', '', amount]
}
if (!kind) {
// Probably a charge from credit card
if (amount.startsWith('+')) {
// keep type + location in the same field for use with payee renaming feature
return [date, `Charge ${type} ${location}`, '', amount]
} else {
return [date, `${type} ${location}`, '', amount]
}
}
if (kind) {
// A transportation fare
return [
date,
TransportationPayee,
`${type} ${location} ${kind} ${exitLocation}`,
amount,
]
}
return [date, 'Format Error', 'Unknown', amount]
}
)
exportToCsv('suica.csv', [csvTitleRow, ...dataRows])
})
// refactored from https://stackoverflow.com/a/24922761
/**
* @typedef {null|string|number|Date} CSVData
* @param {string} filename
* @param {ReadonlyArray<ReadonlyArray<CSVData>>} rows
*/
function exportToCsv(filename, rows) {
const csvFile = rows
.map(row =>
row
.map(item => {
const itemString =
item === null
? ''
: item instanceof Date
? item.toLocaleString()
: item.toString()
const escaped = itemString.replace(/"/g, '""')
return /("|,|\n)/.test(escaped) ? `"${escaped}"` : escaped
})
.join(',')
)
.join('\n')
const blob = new Blob([csvFile], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
const url = URL.createObjectURL(blob)
link.setAttribute('href', url)
link.setAttribute('download', filename)
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment