diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js new file mode 100644 index 0000000..1fed011 --- /dev/null +++ b/docs/.vuepress/config.js @@ -0,0 +1,11 @@ +module.exports = { + title: 'Denis Tsoi', + description: 'Code to eat 🍞', + themeConfig: { + nav: [ + { text: 'CV', link: '/cv' }, + { text: 'talks', link: '/talks' }, + { text: 'Github', link: 'https://github.com/denistsoi' } + ], + } +} \ No newline at end of file diff --git a/docs/cv/readme.md b/docs/cv/readme.md new file mode 100644 index 0000000..892e9ff --- /dev/null +++ b/docs/cv/readme.md @@ -0,0 +1,139 @@ + +::: tip Summary +Software Developer with 5 years industry experience. +Formerly a Senior Precious Metals Dealer in Hong Kong. +Looking for open-minded group of engineers that **love** to code. +::: + +## Relevant Experience + +### 02/2018 - Present +``` +Software Engineer +Scribe Intelligence +``` +- Added API methods using Express/Sequelize/Postgres. +- Used AWS Lambda functions to act as SQS consumer. + - Used Cloudwatch events to periodically poll. +- Assisted with deploying Gentle realignment app. (based on Kaldi). +- Refactored React web app. + +### 12/2016 - 02/2018 +``` +Software Engineer +GDC/Energybox (Product Team) +``` +- Introduced modern web practices, _webpack/npm/automated build steps etc_. +- Implemented custom Mapbox-gl view for application using open-source map tile providers. +- Built proof of concept to improve data processing pipeline using [nedis](https://github.com/tj/nedis) and pm2 workers. + +### 06/2016 - 12/2016 +``` +Sabbatical +Various +``` +- Freelance Contract work for whub, levels, ruemadame and others. +- Developed using Wordpress and PHP scripts. +- Went travelling. + +### 06/2014 - 06/2016 +``` +Software Engineer +Seed Alpha +``` +- Frontend Developer for Fintech App. + - Web Application using MEVN stack (Vue/Mongo/Express). + - Exposed to Elasticsearch (and implemented ES queries). + - First exposure to Docker. +- Implemented chatbot for 2015 RISE conf. + - Leveraged Heroku, Slack API, Yahoo Finance API and WIT.AI. + +### 01/2014 - 05/2014 +``` +Web Developer +Makible +``` +- Introduced to Golang development. +- Focused mainly on Chrome Extension app using ThreeJS. + - and serial api to communicate with Makibox 3D printer. + + +### 08/2013 - 01/2014 +``` +Junior Developer +Imagination +``` +- Frontend Web Development using AngularJS and SCSS. +- Involved with the development of Pacific Place (Admiralty) Touch Table Interface. +- Worked on IE8 compatibility for CityGate website as part of Swire Properties. + +## Other Experience + +### 05/2010 - 08/2013 +``` +Precious Metals Dealer (Senior) +Various +``` + +- In-charge of training and setting up teams for monitoring precious metals volatility. +- Self-taught programming by automating work flows with VBA scripts. +- Took an interest in programming and enrolled into [General Assembly's Frontend Web Development](#_04-2013-08-2013) first part-time course. + +## Education + +### 04/2013 - 08/2013 +``` +General Assembly, Hong Kong +Frontend Web Development Alumni +``` + +- 10 week part time evening course teaching the fundamentals of frontend web development. +- Was successfully hired as a junior developer for [Imagination](#_08-2013-01-2014) before course completion. + +### 07/2011 - 10/2012 +``` +Chinese University of Hong Kong, Chinese Learning Centre +Foundation Certificate in Putonghua for Non-Native Chinese Speakers +``` +- Evening classes for learning Putonghua. +- Completed 5 modules. + +### 04/2012 - 06/2012 +``` +Chinese University of Hong Kong, School of Continuing Professional Studies. +C/C++ and Project +``` +- Evening classes for learning C and C++. + +### 08/2005 - 07/2008 +``` +BA Economics +Lancaster University +``` + +## Community Experience + +### 05/2018 - 06/2018 +``` +Refugeek, BSD +Javascript Instructor +``` +- (5 weeks) weekly evening workshops teaching javascript fundamentals. +- Built cirriculum based on student knowledge. +- Created a PWA using vuepress to host class materials. +- Sponsored students to attend WebConf 2018. + +### 04/2016 - 01/2018 +``` +Hong Kong Web Developers +Committee Member +``` +- Organised monthly Tech talks/venue with hk community speakers. +- Gave numerous talks on development related topics. + +### 07/2012 - 06/2014 +``` +Dim Sum Labs, Hackerspace +Community Organiser +``` +- Organised events for Hackerspace. \ No newline at end of file diff --git a/docs/questions/cpp/cpp-school.md b/docs/questions/cpp/cpp-school.md new file mode 100644 index 0000000..ac224fd --- /dev/null +++ b/docs/questions/cpp/cpp-school.md @@ -0,0 +1,234 @@ +### C++ Input / Output + +Ref: [link](https://practice.geeksforgeeks.org/problems/c-input-output/0/?ref=self) + +Read 2 integer from stdin and display their product to stdout. + +Input: +The first line of input contains integer T denoting the number of test cases. For each test case, there is a line containing two integers a,b. + +Output: +For each test case, there is a line displaying the product of a and b. + +Constraints: +1<=T<=100 +0<=a,b<=100 + +Example: +Input: +3 +2 6 +3 7 +8 3 +Output: +12 +21 +24 + +``` c++ +#include +using namespace std; + +int main() { + //code + + int t, i, input1, input2; + + i = 0; + cin >> t; + + while (i < t) { + cin >> input1 >> input2; + cout << input1 * input2 << endl; + i++; + + } + return 0; +} +``` + +### Power of Pow | Even Number + +Ref: [link](https://practice.geeksforgeeks.org/problems/power-of-pow-even-number/0/?ref=self) + +Given a single integer N, your task is to find the sum of the square of first N even natural Numbers. + +Examples: + +Input : 3 +Output : 56 +22 + 42 + 62 = 56 + +Input : 8 +Output : 816 +22 + 42 + 62 + 82 + 102 + 122 + 142 + 162 +Input: +First line of the input contains an integer T, denoting the number of test cases. Then T test case follows. The only line of each test case contains an integer N. + +Output: +For each test case output the required anser on a new line. + +Constraints: +1<=T<=100 +N<=104 + +Example: +Input: +3 +2 +5 +9 +Output: +20 +220 +1140 + +``` cpp +#include +#include +using namespace std; + +int main() { + //code + int t, i; + cin >> t; + + while (t--) { + cin >> i; + int temp = 0, result = 0; + + for (int c = 1; c <= i; c++) { + temp = c * 2; + result = result + pow(temp, 2); + } + cout << result << endl; + } + return 0; +} +``` + +### odd numbers + +Given a single integer N, your task is to find the sum of the square of first N odd natural Numbers. + +Examples: + +Input : 3 +Output : 35 +12 + 32 + 52 = 35 + +Input : 8 +Output : 680 +12 + 32 + 52 + 72 + 92 + 112 + 132 + 152 +Input: +First line of the input contains an integer T, denoting the number of test cases. Then T test case follows. The only line of each test case contains an integer N. + +Output: +For each test case output the required anser on a new line. + +Constraints: +1<=T<=100 +N<=104 + +Example: +Input: +3 +2 +5 +9 +Output: + +10 +165 +969 + +``` cpp +#include +#include +using namespace std; + +int main() { + //code + int t, i; + cin >> t; + + while (t--) { + cin >> i; + int temp = 0, result = 0; + + for (int c = 1; c <= i; c++) { + temp = (c * 2) - 1; + result = result + pow(temp, 2); + } + cout << result << endl; + } + return 0; +} +``` + +### isogram + +Ref: [link](https://practice.geeksforgeeks.org/problems/check-if-a-string-is-isogram-or-not/0/?ref=self) + +Given a word or phrase, check if it is isogram or not. An Isogram is a word in which no letter occurs more than once. + +Input: + +The first line of input contains an integer T denoting the number of test cases. Each test case consist of one strings in 'lowercase' only, in a separate line. +Output: + +Print 1 if string is Isogram else print 0. +Constraints: + +1 <= T <= 100 + +1 <= |S| <= 100 +Example: + +Input: + +2 + +machine + +geeks + +Output: + +1 + +0 + +Explanation: +For testcase 2: geeks is not an isogram as 'e' appears twice. Hence we print 0. + +### attempt +``` cpp +#include +#include +#include + +using namespace std; + +int main() { + //code + std::map m; + int t, length; + string str; + cin >> t; + + while (t--) { + + std::getline(std::cin, str); + length = str.length(); + if (length > 26) { + return 0; + } + + cout << length << str << endl; + + } + + return 0; +} +``` \ No newline at end of file diff --git a/docs/questions/cracking/chapter-1-Strings.md b/docs/questions/cracking/chapter-1-Strings.md new file mode 100644 index 0000000..18c21c7 --- /dev/null +++ b/docs/questions/cracking/chapter-1-Strings.md @@ -0,0 +1,47 @@ +Palindrome Permuation + +Can the string be permutated to form a palindrome? + +``` js +function isPalindrome(inputString) { + const cleanString = inputString.replace(/\s/g, ''); + const hashMap = {}; + for (let i = 0; i < cleanString.length; i++) { + if (hashMap[cleanString[i]]) { + hashMap[cleanString[i]] += 1; + } else { + hashMap[cleanString[i]] = 1; + } + } + + if (cleanString.length % 2 === 0) { + // all char count must be even + for (let char in hashMap) { + if (hashMap[char] % 2 !== 0) { + return false; + } else { + return true; + } + } + } else { + let count = 0; + // only one odd + for (let char in hashMap) { + if (hashMap[char] % 2 !== 0) { + count++; + } + } + + if (count === 1) { + return true; + } else { + return false; + } + } +} + +console.log(isPalindrome('appa')); +console.log(isPalindrome('cappac')); +console.log(isPalindrome('hpalahp')); + +``` \ No newline at end of file diff --git a/docs/questions/cracking/pre-recursion.md b/docs/questions/cracking/pre-recursion.md new file mode 100644 index 0000000..ba71b5f --- /dev/null +++ b/docs/questions/cracking/pre-recursion.md @@ -0,0 +1,106 @@ +Question: + +Write fibonacci sequence. + +``` js +function fib(n) { + if (n === 1 || n === 2) { + return 1 + } + return fib(n - 1) + fib(n - 2); +} +console.log(fib(1)) +console.log(fib(2)) +console.log(fib(5)) // 1 1 2 3 5 + +``` + +mem'd +``` js +function fib(n) { + let mem = {}; + + if (mem[n] != null) { + return mem[n]; + } + if (n === 1 || n === 2) { + return 1; + } else { + mem[n] = fib(n - 1) + fib(n - 2); + } + return mem[n]; +} + +console.log(fib(30)) // ... 832040 + +console.log(fib(1)) +console.log(fib(5)) // 1 1 2 3 5 +console.log(fib(30)) // ... 832040 +``` + +``` js +function fib(n, memo) { + if (n === 1 || n === 2) { + return 1 + } + + if (memo[n] > 0) return memo[n]; + + memo[n] = fib(n-1, memo) + fib(n-2, memo); + console.log(n, memo[n - 1]); + return memo[n]; +} + +function getFib(n) { + return fib(n, {}); +} + +console.log(getFib(10)) // ... 832040 + +console.log(fib(1)) +console.log(fib(5)) // 1 1 2 3 5 +console.log(fib(30)) // ... 832040 +``` + +``` js + +let mem = {}; + +function fib(n) { + if (mem[n] != null) { + return mem[n]; + } + + if (n === 1 || n === 2) { + return 1; + } + mem[n] = fib(n - 1) + fib(n - 2); + return mem[n]; +} + +console.log(fib(5)) // 1 1 2 3 5 +console.log(fib(1)) +console.log(fib(30)) // ... 832040 +``` + +bottom-up +``` js +function fib(n) { + const arr = []; + if (n === 1 || n === 2) { + return 1; + } + arr[0] = 0; + arr[1] = 1; + arr[2] = 1; + for (let i = 3; i <= n; i++) { + arr[i] = arr[i -1] + arr[i-2]; + } + return arr[n]; +} + +console.log(fib(1)) +console.log(fib(30)) + + +``` \ No newline at end of file diff --git a/docs/questions/csdojo/longest-substring.md b/docs/questions/csdojo/longest-substring.md new file mode 100644 index 0000000..bcba4ec --- /dev/null +++ b/docs/questions/csdojo/longest-substring.md @@ -0,0 +1,27 @@ +given 'aabcddbbbea' find the longest substring such that b:3 is returned. + +``` js +function findlongest(string) { + let max_count = 1; + let max_char; + let count = 1; + + for (let i = 1; i < string.length - 1; i++) { + if (string[i-1] === string[i]) { + count++; + } + + if (max_count < count) { + max_count = count; + max_char = string[i]; + count = 1; + } + } + + const answer = { max_char, max_count }; + return answer; +} + +findlongest('aabcddbbbeaccccdedaa'); +findlongest('absbdbcnabsbdnwneaasdsnand'); +``` \ No newline at end of file diff --git a/docs/questions/csdojo/maximum-subarray.md b/docs/questions/csdojo/maximum-subarray.md new file mode 100644 index 0000000..62faa4e --- /dev/null +++ b/docs/questions/csdojo/maximum-subarray.md @@ -0,0 +1,21 @@ +find the maximum sum of a subarray in the given array +[1,-3,1,2,-1], dismiss non-positive numbers + +```js +// kadane algorithm +function findMaximum(array) { + let global_maximum = array[0]; + let current_maximum = array[0]; + + for (let i = 1; i < array.length; i++) { + let current = array[i]; + current_maximum = current < 0 ? 0 : Math.max(current, current_maximum + current); + global_maximum = Math.max(current_maximum, global_maximum); + } + + return global_maximum; +} + +console.log(findMaximum([1, -3, 1, 2, -1])); +console.log(findMaximum([1, 3, 4, 2, -1, 4, 10, -12, 9, 2])); +``` \ No newline at end of file diff --git a/docs/questions/readme.md b/docs/questions/readme.md new file mode 100644 index 0000000..3f222ca --- /dev/null +++ b/docs/questions/readme.md @@ -0,0 +1,15 @@ +# coding interview questions + +### Topics + +- Big O +- Algorithm Questions +- Arrays and Strings +- Linked Lists +- Stacks and Queues +- Trees and Graphs +- Bit Manipulation +- Math and Logic Puzzles +- Object-Oriented Design +- Recursion and Dynamic Programming +- Sorting and Searching \ No newline at end of file diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/snippets/06/readme.md b/docs/snippets/06/readme.md new file mode 100644 index 0000000..236be10 --- /dev/null +++ b/docs/snippets/06/readme.md @@ -0,0 +1,51 @@ +--- +prev: /en/ +next: false +sidebar: auto +--- + +# Javascript code is single threaded... + +### However, the browser/node uses multiple processes to leverage multithreading on a CPU level. + +i.e. Multiple tabs on Chrome/Firefox. + + + + \ No newline at end of file diff --git a/docs/snippets/07/readme.md b/docs/snippets/07/readme.md new file mode 100644 index 0000000..a515902 --- /dev/null +++ b/docs/snippets/07/readme.md @@ -0,0 +1,14 @@ +# iterators / generators / async iterators + +`for of` + + + +const iterator = dragons[Symbol.iterator]() +iterator.next() + +const dragons = ['']; + +for (const dragon of dragons) { + +} \ No newline at end of file diff --git a/docs/snippets/awk/stats.awk b/docs/snippets/awk/stats.awk new file mode 100644 index 0000000..eee2a66 --- /dev/null +++ b/docs/snippets/awk/stats.awk @@ -0,0 +1,4 @@ +# stats.awk + +{s += $2; h += $6 } +END {print "Total Pop:", s, "\nTotal households:", h, "\nAverage household size:", s/h, "\nAverage Population per zip code:", s/NR} diff --git a/docs/snippets/awk/total.awk b/docs/snippets/awk/total.awk new file mode 100644 index 0000000..91ef2f3 --- /dev/null +++ b/docs/snippets/awk/total.awk @@ -0,0 +1,3 @@ +# total.awk +{s += $2} +END {print "Total:",s} diff --git a/docs/talks/devops/docker.md b/docs/talks/devops/docker.md new file mode 100644 index 0000000..0b8cf5a --- /dev/null +++ b/docs/talks/devops/docker.md @@ -0,0 +1,5 @@ +--- +prev: /en/ +next: false +sidebar: auto +--- \ No newline at end of file diff --git a/docs/talks/devops/kubernetes.md b/docs/talks/devops/kubernetes.md new file mode 100644 index 0000000..0b8cf5a --- /dev/null +++ b/docs/talks/devops/kubernetes.md @@ -0,0 +1,5 @@ +--- +prev: /en/ +next: false +sidebar: auto +--- \ No newline at end of file diff --git a/docs/talks/devops/nginx.md b/docs/talks/devops/nginx.md new file mode 100644 index 0000000..0b8cf5a --- /dev/null +++ b/docs/talks/devops/nginx.md @@ -0,0 +1,5 @@ +--- +prev: /en/ +next: false +sidebar: auto +--- \ No newline at end of file diff --git a/docs/talks/devops/rancher.md b/docs/talks/devops/rancher.md new file mode 100644 index 0000000..0b8cf5a --- /dev/null +++ b/docs/talks/devops/rancher.md @@ -0,0 +1,5 @@ +--- +prev: /en/ +next: false +sidebar: auto +--- \ No newline at end of file diff --git a/docs/talks/devops/readme.md b/docs/talks/devops/readme.md new file mode 100644 index 0000000..0b8cf5a --- /dev/null +++ b/docs/talks/devops/readme.md @@ -0,0 +1,5 @@ +--- +prev: /en/ +next: false +sidebar: auto +--- \ No newline at end of file diff --git a/docs/talks/git/readme.md b/docs/talks/git/readme.md new file mode 100644 index 0000000..5129902 --- /dev/null +++ b/docs/talks/git/readme.md @@ -0,0 +1,205 @@ +--- +prev: /en/ +next: false +sidebar: auto +--- + +## git terminal workshop + +install git +sourcetree gui (git) + +git config user.name +git config user.email + +### init (start a new project) +``` +git init +``` +### clone (download a remote repo to your local machine) +``` +git clone https://github.com/energbox/eb-workshops +``` + +## show stats/logs + +### status (show status) +``` +git status +``` + +### remote (anything to do with remote info) +``` +git remote -v +``` + +add +``` +git remote add +``` + +e.g. +``` +git remote add https://github.com/energybox/eb-workshops +``` + +remove +``` +git remote remove (removes remote url on your local repo) +``` +## add work + +### add (add to commit, but not committed) +``` +git add . (add all files) +``` + +### commit (commit, adds to commit tree) + +``` +git commit -m "Init Commit" +``` +``` +git commit --amend (amend current commit, good when you want to want to change the commit message) +``` + +### reset (there's soft/hard resets) +// when you do `git add` but want to reset the files added +``` +git reset +``` + +``` +undo last commit +$ git commit -m "Something terribly misguided" (1) +$ git reset HEAD~ (2) +<< edit files as necessary >> (3) +$ git add ... (4) +$ git commit -c ORIG_HEAD (5) +``` + +``` +discards all history and changes back to commit +$ git reset --hard +``` + +### push (pushes commits to remote) +``` +git push origin master +``` + +### tag (when you want to add a tag to a commit, good for releases) + +``` +git tag -a -m +``` + +e.g. +``` +git tag -a 0.1.0 -m "Version 0.1.0" (semver) +``` + +### log (show logs) +``` +git log +``` + +### stash (dont commit but save for later, useful when swtiching branches) + +``` +git stash + +``` +``` +git stash pop (pop out) +``` + +``` +git stash drop (dont want anymore) +``` + +### fetch (fetch don't merge) +``` +git fetch +``` + +## more intermediate techniques + +### pull (fetch + merge) +``` +git pull origin master +``` + +### merge (merge two branches together) +``` +git merge develop master +``` + +### rebase (put local changes above another branch, could be remote) +``` +git merge origin/develop +``` + +### checkout (when you want to switch branch) +``` +git checkout branch +``` + +### branch (when you want to create or delete branch) +``` +git branch +git branch -D (delete branch) +``` + + +### diff (show differences) + +``` +shows file differences not yet staged +git diff +``` + +``` +between two branches +git diff +``` + +### cherrypick (select one commit from another branch and add to current branch) + +``` +git cherrypick +``` + +## submodules (advanced topic) +// todo + + +## cheats +``` +[alias] + l = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(bold yellow)%d%C(reset)' --all + amend = commit --amend + sh = stash + pop = stash pop + drop = stash drop + st = status + co = checkout + c = config + p = push origin develop + rb = rebase origin/develop + f = fetch +``` + +## todos +- gifs `git.md` +- submodules +- other stuff I missed + +## further materials + +- https://try.github.io/levels/1/challenges/1 +- https://github.com/pcottle/learnGitBranching +- see `git-cheatsheet.pdf` for cheatsheet + +## author +Denis Tsoi \ No newline at end of file diff --git a/docs/talks/go/readme.md b/docs/talks/go/readme.md new file mode 100644 index 0000000..0b8cf5a --- /dev/null +++ b/docs/talks/go/readme.md @@ -0,0 +1,5 @@ +--- +prev: /en/ +next: false +sidebar: auto +--- \ No newline at end of file diff --git a/docs/talks/node/readme.md b/docs/talks/node/readme.md new file mode 100644 index 0000000..7b7b08f --- /dev/null +++ b/docs/talks/node/readme.md @@ -0,0 +1,18 @@ +--- +prev: /en/ +next: false +sidebar: auto +--- + +# overview + +- fs +- streams +- async / await +- api development +- creating a worker (using queue/stack) + +- http file server +- http tiny +- http api server + diff --git a/docs/talks/py/01/main.py b/docs/talks/py/01/main.py new file mode 100644 index 0000000..e69de29 diff --git a/docs/talks/py/01/readme.md b/docs/talks/py/01/readme.md new file mode 100644 index 0000000..bd98955 --- /dev/null +++ b/docs/talks/py/01/readme.md @@ -0,0 +1,12 @@ +--- +prev: /en/ +next: false +sidebar: auto +--- + +# python basic concepts + +### goals + +- create a yahoo finance cli + diff --git a/docs/talks/py/readme.md b/docs/talks/py/readme.md new file mode 100644 index 0000000..ef5cc53 --- /dev/null +++ b/docs/talks/py/readme.md @@ -0,0 +1,26 @@ +--- +prev: /en/ +next: false +sidebar: auto +--- + +### lists vs tuples + +- tuples = immutable + fixed length +- lists = mutable + dynamic length + +### jupyter notebook + +### Data science concepts + + +## machine learning + +## supervised learning + +## unsupervised learning + +## deep learning & neural net + +## reinforcement learning + diff --git a/docs/talks/readme.md b/docs/talks/readme.md new file mode 100644 index 0000000..dcba26e --- /dev/null +++ b/docs/talks/readme.md @@ -0,0 +1,70 @@ +### Python + +* [Basic Concepts](./py/01) + + + + + + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..c2ca11e --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "personal", + "version": "1.0.0", + "description": "a vuepress site for resume", + "main": "index.js", + "directories": { + "doc": "docs" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..5e6da3f --- /dev/null +++ b/readme.md @@ -0,0 +1,6 @@ +# personal + +A vuepress powered site for my resume and various findings. + +### author +Denis Tsoi \ No newline at end of file diff --git a/sh/clean.sh b/sh/clean.sh new file mode 100644 index 0000000..e69de29 diff --git a/sh/deploy.sh b/sh/deploy.sh new file mode 100644 index 0000000..be8d6d1 --- /dev/null +++ b/sh/deploy.sh @@ -0,0 +1,7 @@ +vuepress build pages +cd pages/.vuepress/dist + +git init +git add . +git commit -m "Deploy" +git push -f git@github.com:denistsoi/rg master:gh-pages \ No newline at end of file