Implement expression-based filtering

master
MendelGusmao 5 years ago
parent 9d93ee47cd
commit 83e58ecb15
  1. 21
      README.md
  2. 7
      cmd/mop/main.go
  3. 66
      filter.go
  4. 9
      layout.go
  5. 22
      line_editor.go
  6. 24
      profile.go
  7. 4
      sorter.go

@ -26,6 +26,8 @@ following keyboard commands:
- Remove stocks from the list.
o Change column sort order.
g Group stocks by advancing/declining issues.
f Set a filtering expression.
F Unset a filtering expression.
? Display help screen.
esc Quit mop.
@ -33,6 +35,25 @@ When prompted please enter comma-delimited list of stock tickers. The
list and other settings are stored in ``.moprc`` file in your ``$HOME``
directory.
#### Filtering
Mop has an realtime expression-based filtering engine that is very easy to use.
At the main screen, press `f` and a prompt will appear. Now you can
a expression that uses the stock properties.
Example:
```last <= 5```
This expression will make Mop show only stocks whose `last` values are less than $5.
The available properties are: `last`, `change`, `changePercent`, `open`, `low`, `high`, `low52`, `high52`, `volume`, `avgVolume`, `pe`, `peX`, `dividend`, `yield`, `mktCap`, `mktCapX` and `advancing`.
The expression **must** return a boolean value, otherwise it will fail.
For detailed information about the syntax, please refer to [Knetic/govaluate#what-operators-and-types-does-this-support](https://github.com/Knetic/govaluate#what-operators-and-types-does-this-support).
To clear the filter, press `Shift+F`.
### Contributing ###

@ -24,6 +24,8 @@ NO WARRANTIES OF ANY KIND WHATSOEVER. SEE THE LICENSE FILE FOR DETAILS.
+ Add stocks to the list.
- Remove stocks from the list.
? Display this help screen.
f Set filtering expression.
F Unset filtering expression.
g Group stocks by advancing/declining issues.
o Change column sort order.
p Pause market data and stock updates.
@ -69,6 +71,11 @@ loop:
} else if event.Ch == '+' || event.Ch == '-' {
lineEditor = mop.NewLineEditor(screen, quotes)
lineEditor.Prompt(event.Ch)
} else if event.Ch == 'f' {
lineEditor = mop.NewLineEditor(screen, quotes)
lineEditor.Prompt(event.Ch)
} else if event.Ch == 'F' {
profile.SetFilter("")
} else if event.Ch == 'o' || event.Ch == 'O' {
columnEditor = mop.NewColumnEditor(screen, quotes)
} else if event.Ch == 'g' || event.Ch == 'G' {

@ -0,0 +1,66 @@
// Copyright (c) 2013-2019 by Michael Dvorkin and contributors. All Rights Reserved.
// Use of this source code is governed by a MIT-style license that can
// be found in the LICENSE file.
package mop
// Filter gets called to sort stock quotes by one of the columns. The
// setup is rather lengthy; there should probably be more concise way
// that uses reflection and avoids hardcoding the column names.
type Filter struct {
profile *Profile // Pointer to where we store sort column and order.
}
// Returns new Filter struct.
func NewFilter(profile *Profile) *Filter {
return &Filter{
profile: profile,
}
}
// Apply builds a list of sort interface based on current sort
// order, then calls sort.Sort to do the actual job.
func (filter *Filter) Apply(stocks []Stock) []Stock {
var filteredStocks []Stock
for _, stock := range stocks {
var values = map[string]interface{}{
"ticker": strings.TrimSpace(stock.Ticker),
"last": m(stock.LastTrade),
"change": c(stock.Change),
"changePercent": c(stock.ChangePct),
"open": m(stock.Open),
"low": m(stock.Low),
"high": m(stock.High),
"low52": m(stock.Low52),
"high52": m(stock.High52),
"volume": m(stock.Volume),
"avgVolume": m(stock.AvgVolume),
"pe": m(stock.PeRatio),
"peX": m(stock.PeRatioX),
"dividend": m(stock.Dividend),
"yield": m(stock.Yield),
"mktCap": m(stock.MarketCap),
"mktCapX": m(stock.MarketCapX),
"advancing": stock.Advancing,
}
result, err := filter.profile.filterExpression.Evaluate(values)
if err != nil {
panic(err)
}
truthy, ok := result.(bool)
if !ok {
panic("Expression `" + filter.profile.Filter + "` should return a boolean value")
}
if truthy {
filteredStocks = append(filteredStocks, stock)
}
}
return filteredStocks
}

@ -28,6 +28,7 @@ type Column struct {
type Layout struct {
columns []Column // List of stock quotes columns.
sorter *Sorter // Pointer to sorting receiver.
filter *Filter // Pointer to filtering receiver.
regex *regexp.Regexp // Pointer to regular expression to align decimal points.
marketTemplate *template.Template // Pointer to template to format market data.
quotesTemplate *template.Template // Pointer to template to format the list of stock quotes.
@ -153,6 +154,14 @@ func (layout *Layout) prettify(quotes *Quotes) []Stock {
}
profile := quotes.profile
if profile.filterExpression != nil {
if layout.filter == nil { // Initialize filter on first invocation.
layout.filter = NewFilter(profile)
}
pretty = layout.filter.Apply(pretty)
}
if layout.sorter == nil { // Initialize sorter on first invocation.
layout.sorter = NewSorter(profile)
}

@ -5,9 +5,10 @@
package mop
import (
`github.com/nsf/termbox-go`
`regexp`
`strings`
`github.com/nsf/termbox-go`
)
// LineEditor kicks in when user presses '+' or '-' to add or delete stock
@ -37,7 +38,16 @@ func NewLineEditor(screen *Screen, quotes *Quotes) *LineEditor {
// are simply ignored. The prompt is displayed on the 3rd line (between the market
// data and the stock quotes).
func (editor *LineEditor) Prompt(command rune) *LineEditor {
prompts := map[rune]string{'+': `Add tickers: `, '-': `Remove tickers: `}
filterPrompt := `Set filter: `
if filter := editor.quotes.profile.Filter; len(filter) > 0 {
filterPrompt = `Set filter (` + filter + `): `
}
prompts := map[rune]string{
'+': `Add tickers: `, '-': `Remove tickers: `,
'f': filterPrompt,
}
if prompt, ok := prompts[command]; ok {
editor.prompt = prompt
editor.command = command
@ -184,6 +194,14 @@ func (editor *LineEditor) execute() *LineEditor {
}
}
}
case 'f':
if len(editor.input) == 0 {
editor.input = editor.quotes.profile.Filter
}
editor.quotes.profile.SetFilter(editor.input)
case 'F':
editor.quotes.profile.SetFilter("")
}
return editor

@ -8,6 +8,8 @@ import (
"encoding/json"
"io/ioutil"
"sort"
"github.com/Knetic/govaluate"
)
// Profile manages Mop program settings as defined by user (ex. list of
@ -20,6 +22,8 @@ type Profile struct {
SortColumn int // Column number by which we sort stock quotes.
Ascending bool // True when sort order is ascending.
Grouped bool // True when stocks are grouped by advancing/declining.
Filter string // Filter in human form
filterExpression *govaluate.EvaluableExpression // The filter as a govaluate expression
selectedColumn int // Stores selected column number when the column editor is active.
filename string // Path to the file in which the configuration is stored
}
@ -36,9 +40,11 @@ func NewProfile(filename string) *Profile {
profile.Tickers = []string{`AAPL`, `C`, `GOOG`, `IBM`, `KO`, `ORCL`, `V`}
profile.SortColumn = 0 // Stock quotes are sorted by ticker name.
profile.Ascending = true // A to Z.
profile.Filter = ""
profile.Save()
} else {
json.Unmarshal(data, profile)
profile.SetFilter(profile.Filter)
}
profile.selectedColumn = -1
@ -120,3 +126,21 @@ func (profile *Profile) Regroup() error {
profile.Grouped = !profile.Grouped
return profile.Save()
}
// SetFilter creates a govaluate.EvaluableExpression.
func (profile *Profile) SetFilter(filter string) {
if len(filter) > 0 {
var err error
profile.filterExpression, err = govaluate.NewEvaluableExpression(filter)
if err != nil {
panic(err)
}
} else if len(filter) == 0 && profile.filterExpression != nil {
profile.filterExpression = nil
}
profile.Filter = filter
profile.Save()
}

@ -208,6 +208,10 @@ func c(str string) float32 {
// When sorting by the market value we must first convert 42B etc. notations
// to proper numeric values.
func m(str string) float32 {
if len(str) == 0 {
return 0
}
multiplier := 1.0
switch str[len(str)-1 : len(str)] { // Check the last character.

Loading…
Cancel
Save