Skip to content

Instantly share code, notes, and snippets.

@spaced
Last active June 29, 2017 19:23
Show Gist options
  • Save spaced/f5662b605c6d42643e3e692c0b76685a to your computer and use it in GitHub Desktop.
Save spaced/f5662b605c6d42643e3e692c0b76685a to your computer and use it in GitHub Desktop.
TogglReport
#! /usr/local/bin/amm
/**
* A ammonite script http://www.lihaoyi.com/Ammonite/#Ammonite-Shell
* For report toggl datas
* HowTo:
* - install ammonite: sudo curl -L -o /usr/local/bin/amm https://git.io/vHaMa && sudo chmod +x /usr/local/bin/amm
* - update script with Toggl-apikey and
* - chmod +x togglReport.sc
* - ./togglReport.sc
*
* Available subcommands:
reportYear
--year Int (default 2017)
reportMonth
--month Int (default current month)
--year Int (default current year)
reportMonthForExcel
--month Int (default current month)
--year Int (default current year)
*/
import ammonite.ops._
import scala.collection.JavaConverters._
import java.time._
import $ivy.`org.scalaj::scalaj-http:2.3.0`, scalaj.http._
import $ivy.`net.sf.biweekly:biweekly:0.6.1`,biweekly._
/**
* Settings
*/
val TogglApiToken = "YourTogglAPIToken"
val WorkspaceId = "YOURWORKSPACEID"
val WorkPerWeekInHour = 42
val WorkPercentage = 0.8
val AnnualLeave: List[(LocalDate, LocalDate)] = List(
(LocalDate.of(2017,1,24), LocalDate.of(2017,1,24)),
(LocalDate.of(2017,4,18), LocalDate.of(2017,4,28))
)
val WorkPerWeekInMS = WorkPerWeekInHour * 60 * 60 * 1000 * WorkPercentage
val WorkPerDayInMS: Int = (WorkPerWeekInMS / 5).toInt
object DateExtension {
object Interval {
def apply( t : (LocalDate, LocalDate)):Interval = Interval(t._1,t._2)
}
case class Interval(from: LocalDate, to: LocalDate) {
def contains(date: LocalDate): Boolean = from == date || to == date || (from.isBefore(date) && to.isAfter(date))
}
}
def fetchFreeDays:List[java.time.LocalDate] = {
//gesetzliche feiertage stadt zug
val resp = Http("https://fcal.ch/privat/fcal_holidays.ics?hl=de&klasse=3&geo=2871").asString
val ical = Biweekly.parse(resp.body).first()
ical.getEvents.asScala.toList.map(_.getDateStart().getValue.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate())
}
def fetchTogglEntries(since: LocalDate) :Map[java.time.LocalDate,Int] = {
val dateFormatter = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd")
def fetchDatas(aggr: List[upickle.Js.Value],page: Int): List[upickle.Js.Value] = {
val resp = Http("https://toggl.com/reports/api/v2/details")
.auth(TogglApiToken, "api_token")
.param("workspace_id", WorkspaceId)
.param("since", since.format(dateFormatter))
.param("user_agent", "api_test")
.param("page", page.toString)
.asString
val parsed = upickle.json.read(resp.body).asInstanceOf[upickle.Js.Obj]
val datas:List[upickle.Js.Value] = parsed("data").arr.toList
val newDatas:List[upickle.Js.Value] = aggr ++ datas
if (parsed("total_count").num.toInt == newDatas.size)
newDatas
else
fetchDatas(newDatas, page +1)
}
val tupleList: Seq[(String,Int)] = fetchDatas(Nil,1).map( d => (d("start").str,d("dur").num.toInt))
val gPerDay = tupleList
.groupBy( t => LocalDate.from(format.DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(t._1)))
.mapValues( _.map(_._2).sum)
gPerDay
}
val oldestOnTop = Ordering.fromLessThan[LocalDate](_ isBefore _)
def formatHH(tInMilli: Int):BigDecimal = {
val hhUnrounded = BigDecimal(tInMilli)/1000/60/60
hhUnrounded.setScale(2, BigDecimal.RoundingMode.HALF_EVEN)
}
object Report {
case class DayReport(date: LocalDate, expected: Int, worked: Int, totalDiff:Int) {
def diff:Int = worked - expected
}
}
val upDownFn = (dr: Report.DayReport) => s"${dr.date}\texpected: ${formatHH(dr.expected)}\tworked: ${formatHH(dr.worked)}\tdiff: ${formatHH(dr.diff)}\taggr: ${formatHH(dr.totalDiff)}"
def reportFor(date: LocalDate, dayReportFormatter: Report.DayReport => String = upDownFn): Unit = {
val togglEntries = fetchTogglEntries(date)
val freeDays = fetchFreeDays
val today = LocalDate.now()
def calcDay(reports: List[Report.DayReport], date: LocalDate): List[Report.DayReport] = {
val expected = date.getDayOfWeek match {
case DayOfWeek.SATURDAY => 0
case DayOfWeek.SUNDAY => 0
case _ if freeDays.contains(date) || AnnualLeave.exists( DateExtension.Interval(_).contains(date) ) => 0
case _ => WorkPerDayInMS
}
val worked = togglEntries.getOrElse(date, 0)
val dayReport = Report.DayReport(date,expected, worked, reports.headOption.map(_.totalDiff).getOrElse(0) + worked - expected )
if (date == today) {
dayReport :: reports
} else {
calcDay(dayReport :: reports, date.plusDays(1))
}
}
calcDay(Nil, date).reverse.foreach{ dr =>
System.out.println(dayReportFormatter(dr))
}
}
@main
def reportYear(year: Int = LocalDate.now().getYear) =
reportFor(LocalDate.of(year, 1, 1))
@main
def reportMonth(month: Int = LocalDate.now().getMonthValue, year: Int = LocalDate.now().getYear) =
reportFor(LocalDate.of(year, month, 1))
@main
def reportMonthForExcel(month: Int = LocalDate.now().getMonthValue, year: Int = LocalDate.now().getYear) =
reportFor(LocalDate.of(year, month, 1), (dr: Report.DayReport) => formatHH(dr.worked).toString())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment