Skip to content

Commit

Permalink
Version 4
Browse files Browse the repository at this point in the history
  • Loading branch information
goosepirate committed Sep 29, 2022
1 parent eb9d3b8 commit 5639471
Show file tree
Hide file tree
Showing 14 changed files with 201 additions and 34 deletions.
68 changes: 62 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ Lox365 is a LibreOffice extension that adds modern spreadsheet functions like XL

![Screenshot](image1.png)

![Screenshot](image2.png)

## Install

1. Download the extension `Lox365.oxt`.
2. Start LibreOffice > Tools > Extension Manager > Add > Select the oxt file > restart LibreOffice
1. Download the extension `Lox365.oxt` from [Releases](https://github.com/goosepirate/lox365/releases).
2. Start LibreOffice > Tools > Extension Manager > Add > Select the oxt file > restart LibreOffice.

## Usage

Expand All @@ -30,11 +32,37 @@ Notes:
4. Not supported: `by_col`, `exactly_once`.
5. Not supported: `match_mode`, `search_mode`.

Note that since LibreOffice Calc does not support [dynamic arrays](https://support.microsoft.com/en-us/office/guidelines-and-examples-of-array-formulas-7d94a64e-3ff3-4686-9372-ecfd5caa57c7), many of the functions provided here are [standard array formulas](https://help.libreoffice.org/latest/en-US/text/scalc/01/04060107.html) (also known as CSE formulas).

## Why

I use these functions quite often in Excel and wanted to use them in LibreOffice too, so I made this. Contributions are welcome.

## More functions
Here are what others are saying about this project:

> Thanks for this; great idea!
— u/timespreader

> Really nice idea.
— Behzat Yildirim

> Very well done to the creator of the extension.
— Jimmy

> Oh, wonderful!
>
> 😀 Thanks for implementing this!
— Arne

> The support of XLOOKUP is a great addition.
— Marius Spix

## Other functions

These functions are not in LibreOffice and not provided by Lox365 but are available in the latest Excel:

Expand All @@ -54,22 +82,50 @@ These functions are already available in LibreOffice:
* SWITCH
* TEXTJOIN

## Reference
## References

Usage

https://wiki.documentfoundation.org/Documentation/HowTo/install_extension

https://wiki.documentfoundation.org/Feature_Comparison:_LibreOffice_-_Microsoft_Office

Media

https://blog.documentfoundation.org/blog/2022/09/23/lox365-extension-xlookup-and-more-for-libreoffice-calc/

https://es.blog.documentfoundation.org/extension-lox365-xlookup-y-mas-para-libreoffice-calc/

https://blog.libreoffice.org.tr/2022/09/23/libreoffice-calc-icin-yeni-bir-eklenti-goosepirate/

https://www.reddit.com/r/libreoffice/comments/x98nqt/lox365_xlookup_for_libreoffice/

https://www.reddit.com/r/libreoffice/comments/xltuio/lox365_extension_xlookup_filter_sort_and_more_for/

https://www.reddit.com/r/opensource/comments/xfdmml/lox365_xlookup_for_libreoffice/

https://twitter.com/LibreOffice/status/1573232603351879682

https://fosstodon.org/@libreoffice/109046849962893237

https://www.facebook.com/libreoffice.org/posts/pfbid07mXEodbV2i32W6JkbRYWdDoyw8sUkiw7cX8QdTLU357AhJKGr9QoH5zKeJUxArkzl

Development

https://bugs.documentfoundation.org/show_bug.cgi?id=126573

https://bugs.documentfoundation.org/show_bug.cgi?id=127293

https://gerrit.libreoffice.org/c/core/+/131905

https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1uno.html

https://wiki.openoffice.org/wiki/Calc/Add-In/Python_How-To

https://wiki.openoffice.org/wiki/Python/Python_Language_Binding

https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1sheet.html

https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1uno.html

https://www.openoffice.org/api/docs/common/ref/com/sun/star/sheet/AddIn.html

https://www.openoffice.org/api/docs/common/ref/com/sun/star/sheet/module-ix.html
15 changes: 15 additions & 0 deletions addin.xcu
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@
</node>
</node>
</node>
<node oor:name="DBG_PY" oor:op="replace">
<prop oor:name="DisplayName"><value xml:lang="en">DBG_PY</value></prop>
<prop oor:name="Description"><value xml:lang="en">For debugging only. Provided by Lox365.</value></prop>
<prop oor:name="Category"><value>Add-in</value></prop>
<node oor:name="Parameters">
<node oor:name="cr" oor:op="replace">
<prop oor:name="DisplayName"><value xml:lang="en">cr</value></prop>
<prop oor:name="Description"><value xml:lang="en">Input 1</value></prop>
</node>
<node oor:name="evalx" oor:op="replace">
<prop oor:name="DisplayName"><value xml:lang="en">evalx</value></prop>
<prop oor:name="Description"><value xml:lang="en">Input 2</value></prop>
</node>
</node>
</node>
<node oor:name="FILTER" oor:op="replace">
<prop oor:name="DisplayName"><value xml:lang="en">FILTER</value></prop>
<prop oor:name="Description"><value xml:lang="en">Filter a range or array. Provided by Lox365.</value></prop>
Expand Down
Binary file modified build/Lox365.oxt
Binary file not shown.
Binary file modified build/interface.rdb
Binary file not shown.
Binary file modified build/interface.urd
Binary file not shown.
2 changes: 1 addition & 1 deletion description.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
xmlns:xlink="http://www.w3.org/1999/xlink">
<identifier value="com.goosepirate.lox365.oxt" />
<icon><default xlink:href="icon.png" /></icon>
<version value="3.0" />
<version value="4.0" />
<publisher><name xlink:href="https://github.com/goosepirate/lox365" lang="en">goosepirate</name></publisher>
<display-name><name lang="en">Lox365</name></display-name>
<extension-description><src xlink:href="extension-description.txt" lang="en"/></extension-description>
Expand Down
Binary file added image2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 7 additions & 3 deletions interface.idl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// #include <com/sun/star/beans/XPropertySet.idl>
// #include <com/sun/star/table/XCellRange.idl>
#include <com/sun/star/table/XCellRange.idl>
#include <com/sun/star/uno/XInterface.idl>

module org { module openoffice { module sheet { module addin {
Expand All @@ -13,6 +13,10 @@ module org { module openoffice { module sheet { module addin {
sequence< sequence< any > > DBG_ECHO3(
[in] any x1
);
sequence< sequence< any > > DBG_PY(
[in] com::sun::star::table::XCellRange cr,
[in] string evalx
);
sequence< sequence< any > > FILTER(
[in] sequence< sequence< any > > array,
[in] sequence< sequence< any > > include,
Expand All @@ -35,8 +39,8 @@ module org { module openoffice { module sheet { module addin {
);
sequence< sequence< any > > XLOOKUP(
[in] any lookupValue,
[in] sequence< sequence< any > > lookupArray,
[in] sequence< sequence< any > > returnArray,
[in] com::sun::star::table::XCellRange lookupArray,
[in] com::sun::star::table::XCellRange returnArray,
[in] any ifNotFound
);
};
Expand Down
58 changes: 50 additions & 8 deletions loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,58 @@
class Lox365(unohelper.Base, XLox365):
def __init__(self, ctx): self.ctx = ctx

def DBG_ECHO (self, *args): return lx.DBG_ECHO(*args)
def DBG_ECHO2(self, *args): return lx.DBG_ECHO2(*args)
def DBG_ECHO3(self, *args): return lx.DBG_ECHO3(*args)
def _get_dataarray(self, cellrange, positions: dict) -> tuple[tuple]:
"""Return the DataArray for the given cell range object and
desired corner positions, which must be a dict with the keys:
left, top, right, bottom.
"""
return cellrange.getCellRangeByPosition(
positions['left'], positions['top'],
positions['right'], positions['bottom'],).DataArray

def FILTER (self, *args): return lx.FILTER(*args)
def SORT (self, *args): return lx.SORT(*args)
def _get_shrunk_corners(self, cellrange) -> dict:
"""Find the rectangular cell range that encloses all computable
content by removing from the given cell range all empty cells
from the bottom and right. Then, return its corner positions
as a dict with the keys: left, top, right, bottom.
Computable content here refers to cells that have:
numeric value, datetime, string, or formula.
"""
address = cellrange.RangeAddress
useful_ranges = cellrange.queryContentCells(23).RangeAddresses
useful_positions = {'left': 0, 'top': 0,
'right': max(range.EndColumn for range in useful_ranges) - address.StartColumn,
'bottom': max(range.EndRow for range in useful_ranges) - address.StartRow,
}
return useful_positions

def DBG_ECHO (self, *args): return lx.DBG_ECHO (*args)
def DBG_ECHO2(self, *args): return lx.DBG_ECHO (*args)
def DBG_ECHO3(self, *args): return lx.DBG_ECHO (*args)
def DBG_PY (self, *args): return lx.DBG_PY (*args)

def FILTER (self, *args): return lx.FILTER (*args)
def SORT (self, *args): return lx.SORT (*args)
def TEXTSPLIT(self, *args): return lx.TEXTSPLIT(*args)
def TOCOL (self, *args): return lx.TOCOL(*args)
def UNIQUE (self, *args): return lx.UNIQUE(*args)
def XLOOKUP (self, *args): return lx.XLOOKUP(*args)
def TOCOL (self, *args): return lx.TOCOL (*args)
def UNIQUE (self, *args): return lx.UNIQUE (*args)

def XLOOKUP(self, *args):
shrunk_corners1 = self._get_shrunk_corners(args[1])
shrunk_corners2 = self._get_shrunk_corners(args[2])
shrunk_corners_common_bottom = max(
shrunk_corners1['bottom'], shrunk_corners2['bottom'])
shrunk_dataarray1 = self._get_dataarray(args[1], {
'left': 0, 'top': 0,
'right': shrunk_corners1['right'],
'bottom': shrunk_corners_common_bottom})
shrunk_dataarray2 = self._get_dataarray(args[2], {
'left': 0, 'top': 0,
'right': shrunk_corners2['right'],
'bottom': shrunk_corners_common_bottom})
args = (args[0], shrunk_dataarray1, shrunk_dataarray2, *args[3:],)
return lx.XLOOKUP(*args)

def createInstance(ctx):
return Lox365(ctx)
Expand Down
1 change: 1 addition & 0 deletions pythonpath/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pytest-benchmark = "*"
icecream = "*"
numpy = "*"
pygal = "*"
sortedcontainers = "*"

[dev-packages]

Expand Down
16 changes: 12 additions & 4 deletions pythonpath/Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 18 additions & 8 deletions pythonpath/lox365.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
# import functools
import pprint

ERR_CALC = '#CALC!'
ERR_NA = '#N/A'

def DBG_ECHO(x1):
return ((repr(x1),),)
def DBG_ECHO2(x1):
return ((repr(x1),),)
def DBG_ECHO3(x1):
return ((repr(x1),),)

def DBG_PY(cr, evalx):
address = cr.RangeAddress
useful_ranges = cr.queryContentCells(23).RangeAddresses
useful_positions = {'left': 0, 'top': 0,
'right': max(range.EndColumn for range in useful_ranges) - address.StartColumn,
'bottom': max(range.EndRow for range in useful_ranges) - address.StartRow
}
useful_dataarray = cr.getCellRangeByPosition(
useful_positions['left'], useful_positions['top'],
useful_positions['right'], useful_positions['bottom']).DataArray
return ((repr(evalx),),)
# return ((pprint.pformat(repr(eval(evalx))),),)

def FILTER(array, include, ifEmpty=ERR_CALC):
if not ifEmpty: ifEmpty = ERR_CALC
if ifEmpty is None: ifEmpty = ERR_CALC
lookup_direction = 0 # 0 is vertical; 1 is horizontal
if len(include) == 1 and len(include[0]) > 1: lookup_direction = 1
import itertools
Expand All @@ -22,8 +32,8 @@ def FILTER(array, include, ifEmpty=ERR_CALC):
return ans if ans else ((ifEmpty,),)

def SORT(array, sortIndex=1, sortOrder=1):
if not sortIndex: sortIndex = 1
if not sortOrder or sortOrder == 1: reverse = False
if sortIndex is None: sortIndex = 1
if sortOrder is None or sortOrder == 1: reverse = False
elif sortOrder == -1: reverse = True
else: return ValueError
return tuple(sorted(array,
Expand All @@ -47,7 +57,7 @@ def UNIQUE(array):
return tuple(dict.fromkeys(array))

def XLOOKUP(lookupValue, lookupArray, returnArray, ifNotFound=ERR_NA):
if not ifNotFound: ifNotFound = ERR_NA
if ifNotFound is None: ifNotFound = ERR_NA
lookup_direction = 0 # 0 is vertical; 1 is horizontal
if len(lookupArray) == 1 and len(lookupArray[0]) > 1: lookup_direction = 1
try:
Expand Down
11 changes: 11 additions & 0 deletions pythonpath/test_lox365.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ def test_XLOOKUP():
(('B',), ('D',), ('F',)),
) == (('D',),)

'''Blank key'''
assert XLOOKUP('',
(('A',), ('',), ('E',), ('',)),
(('B',), ('D',), ('F',), ('H',)),
) == (('D',),)

'''Not found'''
assert XLOOKUP('J',
(('A',), ('C',), ('E',)),
Expand Down Expand Up @@ -118,3 +124,8 @@ def test_XLOOKUP():
(('B',), ('D',), ('F',)),
'Not found',
) == (('Not found',),)
assert XLOOKUP('J',
(('A',), ('C',)),
(('B',), ('D',)),
'',
) == (('',),)
28 changes: 24 additions & 4 deletions pythonpath/test_perf_lox365.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
# pytest-benchmark
from lox365 import *

# def test_XLOOKUP_large_arrays(benchmark):
# benchmark(XLOOKUP, 'C',
# tuple([()]*500000) + (('A',), ('C',), ('E',)) + tuple([()]*500000),
# tuple([()]*500000) + (('B',), ('D',), ('F',)) + tuple([()]*500000),
def test_XLOOKUP_all_rows_early_match(benchmark):
benchmark(XLOOKUP, 'C',
tuple([('A',),('B',),('C',)] + [('',)] * (1048576 - 3)),
tuple([('D',),('E',),('F',)] + [('',)] * (1048576 - 3)))

def test_XLOOKUP_all_rows_late_match(benchmark):
benchmark(XLOOKUP, 'C',
tuple([('',)] * (1048576 - 3) + [('A',),('B',),('C',)]),
tuple([('',)] * (1048576 - 3) + [('D',),('E',),('F',)]))

# def test_XLOOKUP_repeated_lookups_on_same_array(benchmark):
# import numpy as np
# called = 0
# rand_lookup_array = tuple()
# def get_rand_lookup_array():
# nonlocal called
# nonlocal rand_lookup_array
# if called % 100 == 0:
# rand_lookup_array = tuple(map(tuple, np.random.uniform(0,100,100000).reshape(-1,1)))
# called += 1
# return rand_lookup_array
# benchmark(XLOOKUP, np.random.uniform(0,100),
# get_rand_lookup_array(),
# tuple(map(tuple, np.random.uniform(0,100,100000).reshape(-1,1)))
# )

# def test_TEXTSPLIT_large_string(benchmark):
Expand Down

0 comments on commit 5639471

Please sign in to comment.