summaryrefslogtreecommitdiff
path: root/contrib
diff options
context:
space:
mode:
Diffstat (limited to 'contrib')
-rw-r--r--contrib/CSVReader.cs165
-rw-r--r--contrib/Makefile4
-rw-r--r--contrib/ParseCcStmt.cs184
-rw-r--r--contrib/README4
-rwxr-xr-xcontrib/bal21
-rwxr-xr-xcontrib/bal-huquq21
-rwxr-xr-xcontrib/entry16
-rwxr-xr-xcontrib/getquote-uk.py23
-rwxr-xr-xcontrib/getquote.pl19
-rwxr-xr-xcontrib/ledger-du49
-rwxr-xr-xcontrib/repl.sh13
-rwxr-xr-xcontrib/report21
-rwxr-xr-xcontrib/tc7
-rwxr-xr-xcontrib/ti5
-rwxr-xr-xcontrib/to3
-rwxr-xr-xcontrib/trend30
-rw-r--r--contrib/vim/README100
-rw-r--r--contrib/vim/ftplugin/ledger.vim302
-rw-r--r--contrib/vim/syntax/ledger.vim49
19 files changed, 1036 insertions, 0 deletions
diff --git a/contrib/CSVReader.cs b/contrib/CSVReader.cs
new file mode 100644
index 00000000..a22eab06
--- /dev/null
+++ b/contrib/CSVReader.cs
@@ -0,0 +1,165 @@
+// This code is in the public domain. I can't remember where I found it on the Web, but it
+// didn't come with any license.
+
+using System;
+using System.Collections;
+using System.IO;
+using System.Text;
+
+namespace CSVReader {
+
+ /// <summary>
+ /// A data-reader style interface for reading CSV files.
+ /// </summary>
+ public class CSVReader : IDisposable {
+
+ #region Private variables
+
+ private Stream stream;
+ private StreamReader reader;
+
+ #endregion
+
+ /// <summary>
+ /// Create a new reader for the given stream.
+ /// </summary>
+ /// <param name="s">The stream to read the CSV from.</param>
+ public CSVReader(Stream s) : this(s, null) { }
+
+ /// <summary>
+ /// Create a new reader for the given stream and encoding.
+ /// </summary>
+ /// <param name="s">The stream to read the CSV from.</param>
+ /// <param name="enc">The encoding used.</param>
+ public CSVReader(Stream s, Encoding enc) {
+
+ this.stream = s;
+ if (!s.CanRead) {
+ throw new CSVReaderException("Could not read the given CSV stream!");
+ }
+ reader = (enc != null) ? new StreamReader(s, enc) : new StreamReader(s);
+ }
+
+ /// <summary>
+ /// Creates a new reader for the given text file path.
+ /// </summary>
+ /// <param name="filename">The name of the file to be read.</param>
+ public CSVReader(string filename) : this(filename, null) { }
+
+ /// <summary>
+ /// Creates a new reader for the given text file path and encoding.
+ /// </summary>
+ /// <param name="filename">The name of the file to be read.</param>
+ /// <param name="enc">The encoding used.</param>
+ public CSVReader(string filename, Encoding enc)
+ : this(new FileStream(filename, FileMode.Open), enc) { }
+
+ /// <summary>
+ /// Returns the fields for the next row of CSV data (or null if at eof)
+ /// </summary>
+ /// <returns>A string array of fields or null if at the end of file.</returns>
+ public string[] GetCSVLine() {
+
+ string data = reader.ReadLine();
+ if (data == null) return null;
+ if (data.Length == 0) return new string[0];
+
+ ArrayList result = new ArrayList();
+
+ ParseCSVFields(result, data);
+
+ return (string[])result.ToArray(typeof(string));
+ }
+
+ // Parses the CSV fields and pushes the fields into the result arraylist
+ private void ParseCSVFields(ArrayList result, string data) {
+
+ int pos = -1;
+ while (pos < data.Length)
+ result.Add(ParseCSVField(data, ref pos));
+ }
+
+ // Parses the field at the given position of the data, modified pos to match
+ // the first unparsed position and returns the parsed field
+ private string ParseCSVField(string data, ref int startSeparatorPosition) {
+
+ if (startSeparatorPosition == data.Length-1) {
+ startSeparatorPosition++;
+ // The last field is empty
+ return "";
+ }
+
+ int fromPos = startSeparatorPosition + 1;
+
+ // Determine if this is a quoted field
+ if (data[fromPos] == '"') {
+ // If we're at the end of the string, let's consider this a field that
+ // only contains the quote
+ if (fromPos == data.Length-1) {
+ fromPos++;
+ return "\"";
+ }
+
+ // Otherwise, return a string of appropriate length with double quotes collapsed
+ // Note that FSQ returns data.Length if no single quote was found
+ int nextSingleQuote = FindSingleQuote(data, fromPos+1);
+ startSeparatorPosition = nextSingleQuote+1;
+ return data.Substring(fromPos+1, nextSingleQuote-fromPos-1).Replace("\"\"", "\"");
+ }
+
+ // The field ends in the next comma or EOL
+ int nextComma = data.IndexOf(',', fromPos);
+ if (nextComma == -1) {
+ startSeparatorPosition = data.Length;
+ return data.Substring(fromPos);
+ }
+ else {
+ startSeparatorPosition = nextComma;
+ return data.Substring(fromPos, nextComma-fromPos);
+ }
+ }
+
+ // Returns the index of the next single quote mark in the string
+ // (starting from startFrom)
+ private int FindSingleQuote(string data, int startFrom) {
+
+ int i = startFrom-1;
+ while (++i < data.Length)
+ if (data[i] == '"') {
+ // If this is a double quote, bypass the chars
+ if (i < data.Length-1 && data[i+1] == '"') {
+ i++;
+ continue;
+ }
+ else
+ return i;
+ }
+ // If no quote found, return the end value of i (data.Length)
+ return i;
+ }
+
+ /// <summary>
+ /// Disposes the CSVReader. The underlying stream is closed.
+ /// </summary>
+ public void Dispose() {
+ // Closing the reader closes the underlying stream, too
+ if (reader != null) reader.Close();
+ else if (stream != null)
+ stream.Close(); // In case we failed before the reader was constructed
+ GC.SuppressFinalize(this);
+ }
+ }
+
+
+ /// <summary>
+ /// Exception class for CSVReader exceptions.
+ /// </summary>
+ public class CSVReaderException : ApplicationException {
+
+ /// <summary>
+ /// Constructs a new exception object with the given message.
+ /// </summary>
+ /// <param name="message">The exception message.</param>
+ public CSVReaderException(string message) : base(message) { }
+ }
+}
diff --git a/contrib/Makefile b/contrib/Makefile
new file mode 100644
index 00000000..6e4d367a
--- /dev/null
+++ b/contrib/Makefile
@@ -0,0 +1,4 @@
+all: ParseCcStmt.exe
+
+ParseCcStmt.exe: ParseCcStmt.cs CSVReader.cs
+ gmcs -out:ParseCcStmt.exe ParseCcStmt.cs CSVReader.cs
diff --git a/contrib/ParseCcStmt.cs b/contrib/ParseCcStmt.cs
new file mode 100644
index 00000000..c9ad1d55
--- /dev/null
+++ b/contrib/ParseCcStmt.cs
@@ -0,0 +1,184 @@
+/*
+ * Copyright (c) 2003-2008, John Wiegley. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * - Neither the name of New Artisans LLC nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+
+using CSVReader;
+
+/**
+ * @file ParseCcStmt.cs
+ *
+ * @brief Provides a .NET way to turn a CSV report into Ledger transactions.
+ *
+ * I use this code for converting the statements from my own credit card
+ * issuer. I realize it's strange for this to be in C#, but I wrote it
+ * during a phase of C# contracting. The code is solid enough now --
+ * and the Mono project is portable enough -- that I haven't seen the
+ * need to rewrite it into another language like Python.
+ */
+
+namespace JohnWiegley
+{
+ public class Posting
+ {
+ public DateTime Date;
+ public DateTime PostedDate;
+ public string Code;
+ public string Payee;
+ public Decimal Amount;
+ }
+
+ public interface IStatementConverter
+ {
+ List<Posting> ConvertRecords(Stream s);
+ }
+
+ public class ConvertGoldMasterCardStatement : IStatementConverter
+ {
+ public List<Posting> ConvertRecords(Stream s)
+ {
+ List<Posting> posts = new List<Posting>();
+
+ using (CSVReader.CSVReader csv = new CSVReader.CSVReader(s)) {
+ string[] fields;
+ while ((fields = csv.GetCSVLine()) != null) {
+ if (fields[0] == "POSTING DATE")
+ continue;
+
+ Posting post = new Posting();
+
+ post.Date = DateTime.ParseEpost(fields[0], "mm/dd/yy", null);
+ post.PostedDate = DateTime.ParseEpost(fields[1], "mm/dd/yy", null);
+ post.Payee = fields[2].Trim();
+ post.Code = fields[3].Trim();
+ post.Amount = Convert.ToDecimal(fields[4].Trim());
+
+ if (post.Code.Length == 0)
+ post.Code = null;
+
+ posts.Add(post);
+ }
+ }
+ return posts;
+ }
+ }
+
+ public class ConvertMastercardStatement : IStatementConverter
+ {
+ public List<Posting> ConvertRecords(Stream s)
+ {
+ List<Posting> posts = new List<Posting>();
+
+ using (CSVReader.CSVReader csv = new CSVReader.CSVReader(s)) {
+ string[] fields;
+ while ((fields = csv.GetCSVLine()) != null) {
+ Posting post = new Posting();
+
+ post.Date = DateTime.ParseEpost(fields[0], "m/dd/yyyy", null);
+ post.Payee = fields[2].Trim();
+ post.Code = fields[3].Trim();
+ post.Amount = - Convert.ToDecimal(fields[4].Trim());
+
+ if (post.Code.Length == 0)
+ post.Code = null;
+
+ posts.Add(post);
+ }
+ }
+ return posts;
+ }
+ }
+
+ public class PrintPostings
+ {
+ public string DefaultAccount(Posting post) {
+ if (Regex.IsMatch(post.Payee, "IGA"))
+ return "Expenses:Food";
+ return "Expenses:Food";
+ }
+
+ public void Print(string AccountName, string PayAccountName,
+ List<Posting> posts)
+ {
+ foreach (Posting post in posts) {
+ if (post.Amount < 0) {
+ Console.WriteLine("{0} * {1}{2}", post.Date.ToString("yyyy/mm/dd"),
+ post.Code != null ? "(" + post.Code + ") " : "",
+ post.Payee);
+ Console.WriteLine(" {0,-36}{1,12}", AccountName,
+ "$" + (- post.Amount).ToString());
+ Console.WriteLine(" {0}", PayAccountName);
+ } else {
+ Console.WriteLine("{0} {1}{2}", post.Date.ToString("yyyy/mm/dd"),
+ post.Code != null ? "(" + post.Code + ") " : "",
+ post.Payee);
+ Console.WriteLine(" {0,-36}{1,12}", DefaultAccount(post),
+ "$" + post.Amount.ToString());
+ Console.WriteLine(" * {0}", AccountName);
+ }
+ Console.WriteLine();
+ }
+ }
+ }
+
+ public class ParseCcStmt
+ {
+ public static int Main(string[] args)
+ {
+ StreamReader reader = new StreamReader(args[0]);
+ string firstLine = reader.ReadLine();
+
+ string CardAccount = args[1];
+ string BankAccount = args[2];
+
+ IStatementConverter converter;
+
+ if (firstLine.StartsWith("POSTING DATE")) {
+ converter = new ConvertGoldMasterCardStatement();
+ } else {
+ converter = new ConvertMastercardStatement();
+ }
+
+ reader = new StreamReader(args[0]);
+ List<Posting> posts = converter.ConvertRecords(reader.BaseStream);
+
+ PrintPostings printer = new PrintPostings();
+ printer.Print(CardAccount, BankAccount, posts);
+
+ return 0;
+ }
+ }
+}
diff --git a/contrib/README b/contrib/README
new file mode 100644
index 00000000..6108afbf
--- /dev/null
+++ b/contrib/README
@@ -0,0 +1,4 @@
+This scripts are provided just to give some ideas. They probably need
+to be modified to better suit your environment. Beware!
+
+John
diff --git a/contrib/bal b/contrib/bal
new file mode 100755
index 00000000..423e3e41
--- /dev/null
+++ b/contrib/bal
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+switch="-c"
+limit="-t (/Liabilities/?a<0:Ua>100)&a"
+
+if [ "$1" = "-C" -o "$1" = "-U" ]; then
+ switch="$1"
+ shift
+elif [ "$1" = "-b" -o "$1" = "-e" -o "$1" = "-p" ]; then
+ switch="$1 $2"
+ shift 2
+fi
+
+accts="$@"
+if [ -z "$accts" ]; then
+ accts="-Equity -Income -Expenses"
+else
+ limit=""
+fi
+
+ledger -VQ $switch $limit -s -S "-UT" balance $accts
diff --git a/contrib/bal-huquq b/contrib/bal-huquq
new file mode 100755
index 00000000..fad2854a
--- /dev/null
+++ b/contrib/bal-huquq
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+switch="-c"
+limit="-t (/Liabilities/?(/Huquq/?a/P{2.22AU}<={-1.0}:a<0):Ua>100)&a"
+
+if [ "$1" = "-C" -o "$1" = "-U" ]; then
+ switch="$1"
+ shift
+elif [ "$1" = "-b" -o "$1" = "-e" -o "$1" = "-p" ]; then
+ switch="$1 $2"
+ shift 2
+fi
+
+accts="$@"
+if [ -z "$accts" ]; then
+ accts="-Equity -Income -Expenses"
+else
+ limit=""
+fi
+
+ledger -VQ $switch $limit -s -S "-UT" balance $accts
diff --git a/contrib/entry b/contrib/entry
new file mode 100755
index 00000000..ef1869da
--- /dev/null
+++ b/contrib/entry
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+if [ -z "$LEDGER" -o ! -r "$LEDGER" ]; then
+ echo Please set your LEDGER environment variable.
+fi
+
+line=`wc -l $LEDGER | awk '{print $1}'`
+
+if ledger xact "$@" > /tmp/xact; then
+ cat /tmp/xact >> $LEDGER
+else
+ echo "$@" >> $LEDGER
+fi
+rm /tmp/xact
+
+vi +$line $LEDGER
diff --git a/contrib/getquote-uk.py b/contrib/getquote-uk.py
new file mode 100755
index 00000000..a69d4e7d
--- /dev/null
+++ b/contrib/getquote-uk.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import urllib, string, sys
+
+def download(sym):
+ url = "http://uk.old.finance.yahoo.com/d/quotes.csv?s="
+ url += sym + "&f=sl1d1t1c1ohgv&e=.csv"
+ f = urllib.urlopen(url, proxies={})
+ info = f.read()
+ f.close()
+ fields = string.split(info, ',')
+ result = float(fields[1])/100
+ return result
+
+
+sym = sys.argv[1]
+sym = sym.replace('_', '.')
+if sym == '£':
+ print '£1.00'
+else:
+ try: print "£" +str(download(sym))
+ except: pass
diff --git a/contrib/getquote.pl b/contrib/getquote.pl
new file mode 100755
index 00000000..8e3bb678
--- /dev/null
+++ b/contrib/getquote.pl
@@ -0,0 +1,19 @@
+#!/usr/bin/env perl
+
+$timeout = 60;
+
+use Finance::Quote;
+use POSIX qw(strftime localtime time);
+
+$q = Finance::Quote->new;
+$q->timeout($timeout);
+$q->require_labels(qw/price/);
+
+%quotes = $q->fetch("nasdaq", $ARGV[0]);
+if ($quotes{$ARGV[0], "price"}) {
+ print strftime('%Y/%m/%d %H:%M:%S', localtime(time()));
+ print " ", $ARGV[0], " ";
+ print "\$", $quotes{$ARGV[0], "price"}, "\n";
+} else {
+ exit 1;
+}
diff --git a/contrib/ledger-du b/contrib/ledger-du
new file mode 100755
index 00000000..580e916e
--- /dev/null
+++ b/contrib/ledger-du
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+
+import string
+import sys
+import os
+import time
+
+from stat import *
+from os.path import *
+
+def report_file(path):
+ dir_elems = string.split(dirname(path), os.sep)
+ if dir_elems[0] == "." or dir_elems[0] == "":
+ dir_elems = dir_elems[1 :]
+ account = string.join(dir_elems, ":")
+
+ info = os.stat(path)
+ print time.strftime("%Y/%m/%d", time.localtime(info[ST_MTIME])),
+
+ print basename(path)
+ print " ", account, " ", info[ST_SIZE], "b"
+ print " Equity:Files"
+ print
+
+def find_files(path):
+ xacts = os.listdir(path)
+ for xact in xacts:
+ xact = join(path, xact)
+ if not islink(xact):
+ if isdir(xact) and xact != "/proc":
+ find_files(xact)
+ else:
+ report_file(xact)
+
+args = sys.argv[1:]
+if len(args):
+ paths = args
+else:
+ paths = ["."]
+
+print """
+C 1.00 Kb = 1024 b
+C 1.00 Mb = 1024 Kb
+C 1.00 Gb = 1024 Mb
+C 1.00 Tb = 1024 Gb
+"""
+
+for path in paths:
+ find_files(path)
diff --git a/contrib/repl.sh b/contrib/repl.sh
new file mode 100755
index 00000000..42fb54c6
--- /dev/null
+++ b/contrib/repl.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+EXEC=$(which ledger)
+if [[ -z "$EXEC" ]]; then
+ EXEC=$HOME/Products/ledger/ledger
+fi
+
+if [[ ! -x "$EXEC" ]]; then
+ echo Cannot find Ledger executable
+ exit 1
+fi
+
+LESS=--quit-if-one-screen exec $EXEC --pager less "$@"
diff --git a/contrib/report b/contrib/report
new file mode 100755
index 00000000..24418cdc
--- /dev/null
+++ b/contrib/report
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+# This script facilities plotting of a ledger register report. If you
+# use OS/X, and have AquaTerm installed, you will probably want to set
+# LEDGER_TERM to "aqua".
+#
+# Examples of use:
+#
+# report -j -M reg food # plot monthly food costs
+# report -J reg checking # plot checking account balance
+
+if [ -z "$LEDGER_TERM" ]; then
+ LEDGER_TERM="x11 persist"
+fi
+
+(cat <<EOF; ledger "$@") | gnuplot
+ set terminal $LEDGER_TERM
+ set xdata time
+ set timefmt "%Y/%m/%d"
+ plot "-" using 1:2 with lines
+EOF
diff --git a/contrib/tc b/contrib/tc
new file mode 100755
index 00000000..c24be99a
--- /dev/null
+++ b/contrib/tc
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+timeclock out
+
+proj="$1"
+shift
+timeclock in "$proj" "$@"
diff --git a/contrib/ti b/contrib/ti
new file mode 100755
index 00000000..a7214e65
--- /dev/null
+++ b/contrib/ti
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+proj="$1"
+shift
+timeclock in "$proj" "$@"
diff --git a/contrib/to b/contrib/to
new file mode 100755
index 00000000..3198db3c
--- /dev/null
+++ b/contrib/to
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+timeclock out "$@"
diff --git a/contrib/trend b/contrib/trend
new file mode 100755
index 00000000..3c189c0b
--- /dev/null
+++ b/contrib/trend
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+# This script requires Python support.
+#
+# To use, just run "trend" with the accounts to compute the trend for:
+#
+# trend dining
+#
+# The trend values are not terribly meaningful, but this gives an
+# example of how Python can be used to create more complex reports.
+
+ledger --import-stdin -T "@rdev()" reg "$@" <<EOF
+import ledger
+
+mean = ledger.parse_value_expr ("AT")
+last_mean = None
+last_dev = None
+
+def rdev (details):
+ global last_mean, last_dev
+ mval = mean.compute (details)
+ if last_mean is None:
+ dev = ledger.Value ()
+ else:
+ dev = mval - last_mean
+ dev = (last_dev + dev) / 2
+ last_mean = mval
+ last_dev = dev
+ return dev
+EOF
diff --git a/contrib/vim/README b/contrib/vim/README
new file mode 100644
index 00000000..4da73ea6
--- /dev/null
+++ b/contrib/vim/README
@@ -0,0 +1,100 @@
+
+This is the ledger filetype for vim.
+Copy each file to the corresponding directory in your ~/.vim directory.
+Then include the following line in your .vimrc or in ~/.vim/filetype.vim
+ au BufNewFile,BufRead *.ldg,*.ledger setf ledger
+You can also use a modeline like this in every ledger file
+ vim:filetype=ledger
+
+Configuration
+======================================================================
+Include the following let-statements somewhere in your .vimrc
+to modify the behaviour of the ledger filetype.
+
+* Number of colums that will be used to display the foldtext.
+ Set this when you think that the amount is too far off to the right.
+ let g:ledger_maxwidth = 80
+
+* String that will be used to fill the space between account name
+ and amount in the foldtext. Set this to get some kind of lines
+ or visual aid.
+ let g:ledger_fillstring = ' -'
+ My special tip is to use so-called digraphs:
+ Press <C-K> followed by the two-characters key sequence below.
+ (in insert-mode)
+ '. = ˙ or ': = ¨ --> ˙˙˙˙˙˙ or ¨¨¨¨¨¨
+ ', = ¸ --> ¸¸¸¸¸¸
+ .M = · --> ······
+ >> = » --> »»»»»»
+ All those look rather unobstrusive
+ and provide a good visual aid to find the correct amount.
+
+* If you want the account completion to be sorted by level of detail/depth
+ instead of alphabetical, include the following line:
+ let g:ledger_detailed_first = 1
+
+Completion
+======================================================================
+Omni completion is implemented for account names and tags.
+
+Accounts
+----------------------------------------------------------------------
+Account names are matched by the start of every sub-level.
+When you insert an account name like this:
+ Asse<C-X><C-O>
+You will get a list of top-level accounts that start like this.
+
+Go ahead and try something like:
+ As:Ban:Che<C-X><C-O>
+When you have an account like this, 'Assets:Bank:Checking' should show up.
+
+When you want to complete on a virtual transaction,
+it's currently best to keep the cursor in front of the closing bracket.
+Of course you can insert the closing bracket after calling the completion, too.
+
+Tags
+----------------------------------------------------------------------
+The support for completing tags is pretty basic right now
+but it's useful to keep the spelling of your tags consistent.
+You can call the completion after the ';' to get a list of all tags.
+When you have a list of tags (:like: :this:) you can call
+the completion too and everything up to the last ':' (excluding whitespace)
+will be considered the beginning of the tag to search for.
+
+Revision history (major changes)
+======================================================================
+ 2009-06-23 & 2009-06-25
+ J. Klähn: Omni-Completion for account names and tags
+ 2009-06-17 J. Klähn: Highlight account text
+ Updated documentation and added fillstring option.
+ 2009-06-15 J. Klähn: Split into multiple files
+ 2009-06-12 J. Klähn: Use all available columns for foldtext
+ Also rewrote foldtext generation.
+ 2009-03-25 J. Klähn: Allow Metadata
+ in transactions and postings (Ledger 3.0)
+ Also fixed alignment for multi-byte-characters
+ 2009-01-28 S.Karrmann: minor fixes
+ 2009-01-27 third version by S.Karrmann.
+ better extraction of the amount of the posting
+ decimal separator can be one of '.' and ','.
+ 2005-02-05 first version (partly copied from ledger.vim 0.0.1)
+
+License
+======================================================================
+Copyright 2009 by Johann Klähn
+Copyright 2009 by Stefan Karrmann
+Copyright 2005 by Wolfgang Oertl
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+
diff --git a/contrib/vim/ftplugin/ledger.vim b/contrib/vim/ftplugin/ledger.vim
new file mode 100644
index 00000000..d75a6869
--- /dev/null
+++ b/contrib/vim/ftplugin/ledger.vim
@@ -0,0 +1,302 @@
+" Vim filetype plugin file
+" filetype: ledger
+" Version: 0.1.0
+" by Johann Klähn; Use according to the terms of the GPL>=2.
+" vim:ts=2:sw=2:sts=2:foldmethod=marker
+
+if exists("b:did_ftplugin")
+ finish
+endif
+
+let b:did_ftplugin = 1
+
+let b:undo_ftplugin = "setlocal ".
+ \ "foldmethod< foldtext< ".
+ \ "include< comments< omnifunc< "
+
+" don't fill fold lines --> cleaner look
+setl fillchars="fold: "
+setl foldtext=LedgerFoldText()
+setl foldmethod=syntax
+setl include=^!include
+setl comments=b:;
+setl omnifunc=LedgerComplete
+
+" You can set a maximal number of columns the fold text (excluding amount)
+" will use by overriding g:ledger_maxwidth in your .vimrc.
+" When maxwidth is zero, the amount will be displayed at the far right side
+" of the screen.
+if !exists('g:ledger_maxwidth')
+ let g:ledger_maxwidth = 0
+endif
+
+if !exists('g:ledger_fillstring')
+ let g:ledger_fillstring = ' '
+endif
+
+" If enabled this will list the most detailed matches at the top {{{
+" of the completion list.
+" For example when you have some accounts like this:
+" A:Ba:Bu
+" A:Bu:Bu
+" and you complete on A:B:B normal behaviour may be the following
+" A:B:B
+" A:Bu:Bu
+" A:Bu
+" A:Ba:Bu
+" A:Ba
+" A
+" with this option turned on it will be
+" A:B:B
+" A:Bu:Bu
+" A:Ba:Bu
+" A:Bu
+" A:Ba
+" A
+" }}}
+if !exists('g:ledger_detailed_first')
+ let g:ledger_detailed_first = 0
+endif
+
+let s:rx_amount = '\('.
+ \ '\%([0-9]\+\)'.
+ \ '\%([,.][0-9]\+\)*'.
+ \ '\|'.
+ \ '[,.][0-9]\+'.
+ \ '\)'.
+ \ '\s*\%([[:alpha:]¢$€£]\+\s*\)\?'.
+ \ '\%(\s*;.*\)\?$'
+
+function! LedgerFoldText() "{{{1
+ " find amount
+ let amount = ""
+ let lnum = v:foldstart
+ while lnum <= v:foldend
+ let line = getline(lnum)
+
+ " Skip metadata/leading comment
+ if line !~ '^\%(\s\+;\|\d\)'
+ " No comment, look for amount...
+ let groups = matchlist(line, s:rx_amount)
+ if ! empty(groups)
+ let amount = groups[1]
+ break
+ endif
+ endif
+ let lnum += 1
+ endwhile
+
+ let fmt = '%s %s '
+ " strip whitespace at beginning and end of line
+ let foldtext = substitute(getline(v:foldstart),
+ \ '\(^\s\+\|\s\+$\)', '', 'g')
+
+ " number of columns foldtext can use
+ let columns = s:get_columns(0)
+ if g:ledger_maxwidth
+ let columns = min([columns, g:ledger_maxwidth])
+ endif
+ let columns -= s:multibyte_strlen(printf(fmt, '', amount))
+
+ " add spaces so the text is always long enough when we strip it
+ " to a certain width (fake table)
+ if strlen(g:ledger_fillstring)
+ " add extra spaces so fillstring aligns
+ let filen = s:multibyte_strlen(g:ledger_fillstring)
+ let folen = s:multibyte_strlen(foldtext)
+ let foldtext .= repeat(' ', filen - (folen%filen))
+
+ let foldtext .= repeat(g:ledger_fillstring,
+ \ s:get_columns(0)/filen)
+ else
+ let foldtext .= repeat(' ', s:get_columns(0))
+ endif
+
+ " we don't use slices[:5], because that messes up multibyte characters
+ let foldtext = substitute(foldtext, '.\{'.columns.'}\zs.*$', '', '')
+
+ return printf(fmt, foldtext, amount)
+endfunction "}}}
+
+function! LedgerComplete(findstart, base) "{{{1
+ if a:findstart
+ let lnum = line('.')
+ let line = getline('.')
+ let lastcol = col('.') - 2
+ if line =~ '^\d' "{{{2 (date / payee / description)
+ let b:compl_context = 'payee'
+ return -1
+ elseif line =~ '^\s\+;' "{{{2 (metadata / tags)
+ let b:compl_context = 'meta-tag'
+ let first_possible = matchend(line, '^\s\+;')
+
+ " find first column of text to be replaced
+ let firstcol = lastcol
+ while firstcol >= 0
+ if firstcol <= first_possible
+ " Stop before the ';' don't ever include it
+ let firstcol = first_possible
+ break
+ elseif line[firstcol] =~ ':'
+ " Stop before first ':'
+ let firstcol += 1
+ break
+ endif
+
+ let firstcol -= 1
+ endwhile
+
+ " strip whitespace starting from firstcol
+ let end_of_whitespace = matchend(line, '^\s\+', firstcol)
+ if end_of_whitespace != -1
+ let firstcol = end_of_whitespace
+ endif
+
+ return firstcol
+ elseif line =~ '^\s\+' "{{{2 (account)
+ let b:compl_context = 'account'
+ if matchend(line, '^\s\+\%(\S \S\|\S\)\+') <= lastcol
+ " only allow completion when in or at end of account name
+ return -1
+ endif
+ " the start of the first non-blank character
+ " (excluding virtual-transaction-marks)
+ " is the beginning of the account name
+ return matchend(line, '^\s\+[\[(]\?')
+ else "}}}
+ return -1
+ endif
+ else
+ if b:compl_context == 'account' "{{{2 (account)
+ unlet! b:compl_context
+ let hierarchy = split(a:base, ':')
+ if a:base =~ ':$'
+ call add(hierarchy, '')
+ endif
+
+ let results = LedgerFindInTree(LedgerGetAccountHierarchy(), hierarchy)
+ " sort by alphabet and reverse because it will get reversed one more time
+ let results = reverse(sort(results))
+ if g:ledger_detailed_first
+ let results = sort(results, 's:sort_accounts_by_depth')
+ endif
+ call add(results, a:base)
+ return reverse(results)
+ elseif b:compl_context == 'meta-tag' "{{{2
+ unlet! b:compl_context
+ let results = [a:base]
+ call extend(results, sort(s:filter_items(keys(LedgerGetTags()), a:base)))
+ return results
+ else "}}}
+ unlet! b:compl_context
+ return []
+ endif
+ endif
+endf "}}}
+
+function! LedgerFindInTree(tree, levels) "{{{1
+ if empty(a:levels)
+ return []
+ endif
+ let results = []
+ let currentlvl = a:levels[0]
+ let nextlvls = a:levels[1:]
+ let branches = s:filter_items(keys(a:tree), currentlvl)
+ for branch in branches
+ call add(results, branch)
+ if !empty(nextlvls)
+ for result in LedgerFindInTree(a:tree[branch], nextlvls)
+ call add(results, branch.':'.result)
+ endfor
+ endif
+ endfor
+ return results
+endf "}}}
+
+function! LedgerGetAccountHierarchy() "{{{1
+ let hierarchy = {}
+ let accounts = s:grep_buffer('^\s\+\zs[^[:blank:];]\%(\S \S\|\S\)\+\ze')
+ for name in accounts
+ " remove virtual-transaction-marks
+ let name = substitute(name, '\%(^\s*[\[(]\?\|[\])]\?\s*$\)', '', 'g')
+ let last = hierarchy
+ for part in split(name, ':')
+ let last[part] = get(last, part, {})
+ let last = last[part]
+ endfor
+ endfor
+ return hierarchy
+endf "}}}
+
+function! LedgerGetTags() "{{{1
+ let alltags = {}
+ let metalines = s:grep_buffer('^\s\+;\s*\zs.*$')
+ for line in metalines
+ " (spaces at beginning are stripped by matchstr!)
+ if line[0] == ':'
+ " multiple tags
+ for val in split(line, ':')
+ if val !~ '^\s*$'
+ let name = s:strip_spaces(val)
+ let alltags[name] = get(alltags, name, [])
+ endif
+ endfor
+ elseif line =~ '^.*:.*$'
+ " line with tag=value
+ let name = s:strip_spaces(split(line, ':')[0])
+ let val = s:strip_spaces(join(split(line, ':')[1:], ':'))
+ let values = get(alltags, name, [])
+ call add(values, val)
+ let alltags[name] = values
+ endif
+ endfor
+ return alltags
+endf "}}}
+
+" Helper functions {{{1
+
+" return length of string with fix for multibyte characters
+function! s:multibyte_strlen(text) "{{{2
+ return strlen(substitute(a:text, ".", "x", "g"))
+endfunction "}}}
+
+" get # of visible/usable columns in current window
+function! s:get_columns(win) "{{{2
+ " As long as vim doesn't provide a command natively,
+ " we have to compute the available columns.
+ " see :help todo.txt -> /Add argument to winwidth()/
+ " FIXME: Although this will propably never be used with debug mode enabled
+ " this should take the signs column into account (:help sign.txt)
+ let columns = (winwidth(a:win) == 0 ? 80 : winwidth(a:win)) - &foldcolumn
+ if &number
+ " line('w$') is the line number of the last line
+ let columns -= max([len(line('w$'))+1, &numberwidth])
+ endif
+ return columns
+endfunction "}}}
+
+" remove spaces at start and end of string
+function! s:strip_spaces(text) "{{{2
+ return matchstr(a:text, '^\s*\zs\S\%(.*\S\)\?\ze\s*$')
+endf "}}}
+
+" return only those items that start with a specified keyword
+function! s:filter_items(list, keyword) "{{{2
+ return filter(a:list, 'v:val =~ ''^\V'.substitute(a:keyword, '\\', '\\\\', 'g').'''')
+endf "}}}
+
+" return all lines matching an expression, returning only the matched part
+function! s:grep_buffer(expression) "{{{2
+ let lines = map(getline(1, '$'), 'matchstr(v:val, '''.a:expression.''')')
+ return filter(lines, 'v:val != ""')
+endf "}}}
+
+function! s:sort_accounts_by_depth(name1, name2) "{{{2
+ let depth1 = s:count_expression(a:name1, ':')
+ let depth2 = s:count_expression(a:name2, ':')
+ return depth1 == depth2 ? 0 : depth1 > depth2 ? 1 : -1
+endf "}}}
+
+function! s:count_expression(text, expression) "{{{2
+ return len(split(a:text, a:expression, 1))-1
+endf "}}}
diff --git a/contrib/vim/syntax/ledger.vim b/contrib/vim/syntax/ledger.vim
new file mode 100644
index 00000000..8914cf2a
--- /dev/null
+++ b/contrib/vim/syntax/ledger.vim
@@ -0,0 +1,49 @@
+" Vim syntax file
+" filetype: ledger
+" Version: 0.1.0
+" by Johann Klähn; Use according to the terms of the GPL>=2.
+" by Stefan Karrmann; Use according to the terms of the GPL>=2.
+" by Wolfgang Oertl; Use according to the terms of the GPL>=2.
+" Revision history
+" 2009-06-12 J. Klähn: Use all available columns for foldtext
+" 2009-03-25 J. Klähn: Allow Metadata
+" in transactions and postings (Ledger 3.0)
+" Also fixed alignment for multi-byte-characters
+" 2009-01-28 S.Karrmann: minor fixes
+" 2009-01-27 third version by S.Karrmann.
+" better extraction of the amount of the posting
+" decimal separator can be one of '.' and ','.
+" 2005-02-05 first version (partly copied from ledger.vim 0.0.1)
+" vim:ts=2:sw=2:sts=2:foldmethod=marker
+
+if version < 600
+ syntax clear
+elseif exists("b:current_sytax")
+ finish
+endif
+
+" for debugging
+syntax clear
+
+" region: a transaction containing postings
+syn region transNorm start=/^[[:digit:]~]/ skip=/^\s/ end=/^/
+ \ fold keepend transparent contains=transDate, Metadata, Posting
+syn match transDate /^\d\S\+/ contained
+syn match Metadata /^\s\+;.*/ contained
+syn match Comment /^;.*$/
+" every space in an account name shall be surrounded by two non-spaces
+" every account name ends with a tab, two spaces or the end of the line
+syn match Account /^\s\+\zs\%(\S \S\|\S\)\+\ze\%([ ]\{2,}\|\t\s*\|\s*$\)/ contained
+syn match Posting /^\s\+[^[:blank:];].*$/ contained transparent contains=Account
+
+
+highlight default link transDate Question
+highlight default link Metadata PreProc
+highlight default link Comment Comment
+highlight default link Account Identifier
+
+" syncinc is easy: search for the first transaction.
+syn sync clear
+syn sync match ledgerSync grouphere transNorm "^\d"
+
+let b:current_syntax = "ledger"