Skip to content

Commit

Permalink
Server datetime fix (#221)
Browse files Browse the repository at this point in the history
* datetime fix

* handle custom functions dates

* use cultureInfoName instead of contentLanguage

* move/add cultureInfo and dateFormat to xlwings.js and print it

* include date_format in function body

* fix docstring

* bumped python dependencies
  • Loading branch information
fzumstein authored Jan 9, 2025
1 parent 53300b6 commit beb146a
Show file tree
Hide file tree
Showing 10 changed files with 71 additions and 64 deletions.
2 changes: 1 addition & 1 deletion DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ globalThis.socket = io("https://127.0.0.1:8001", {
## Install the xlwings Python package from its repo into xlwings-server in editable mode

- xlwings: delete `project.toml` temporarily
- xlwings: replace `version = "dev"` with the version from `xlwings.js`
- xlwings: replace `version = "dev"` with `0.0.0`
- xlwings-server: `pip uninstall xlwings`
- xlwings-server: `cd ~/dev/xlwings && python setup.py develop`
- xlwings: undo deletion of `project.toml`
Expand Down
11 changes: 7 additions & 4 deletions app/custom_functions/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""

import asyncio
import datetime as dt
import sys
from pathlib import Path
from typing import Annotated
Expand Down Expand Up @@ -35,18 +36,20 @@ def hello(name):
@func
@arg("rows", doc="The number of rows in the returned array.")
@arg("cols", doc="The number of columns in the returned array.")
@ret(index=False)
def standard_normal(rows, cols):
"""Returns an array of standard normally distributed pseudo random numbers"""
rng = np.random.default_rng()
matrix = rng.standard_normal(size=(rows, cols))
df = pd.DataFrame(matrix, columns=[f"col{i+1}" for i in range(matrix.shape[1])])
date_rng = pd.date_range(start=dt.datetime(2025, 6, 15), periods=rows, freq="D")
df = pd.DataFrame(
matrix, columns=[f"col{i+1}" for i in range(matrix.shape[1])], index=date_rng
)
return df


# 3) Reading a pandas DataFrames
@func
@arg("df", pd.DataFrame, index=False)
@arg("df", pd.DataFrame)
def correl(df):
"""Like CORREL, but it works on whole matrices instead of just 2 arrays."""
return df.corr()
Expand All @@ -55,7 +58,7 @@ def correl(df):
# 4) Type hints: this is the same example as 3), but using type hints instead of
# decorators. You could also use type hints and decorators together. In this sample, we
# are storing the Annotated type hint outside of the function, so it is easy to reuse.
Df = Annotated[pd.DataFrame, {"index": False}]
Df = Annotated[pd.DataFrame, {"index": True}]


@func
Expand Down
2 changes: 1 addition & 1 deletion app/custom_scripts/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def show_alert(book: xw.Book):
def setup_custom_functions(book: xw.Book):
prefix = f"{settings.functions_namespace}"
if settings.environment != "prod":
prefix += f"_{settings.environment}"
prefix += f"_{settings.environment}".upper()
sheet = book.sheets.add()
sheet["A3"].value = f'={prefix}.HELLO("xlwings")'
sheet["A5"].value = f"={prefix}.STANDARD_NORMAL(3, 4)"
Expand Down
7 changes: 2 additions & 5 deletions app/static/js/core/custom-functions-code.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ const debug = false;
let invocations = new Set();
let bodies = new Set();
let runtime;
let contentLanguage;
let socket = null;

Office.onReady(function (info) {
Expand Down Expand Up @@ -47,9 +46,6 @@ Office.onReady(function (info) {
} else {
runtime = "1.1";
}

// Content Language
contentLanguage = Office.context.contentLanguage;
});

function flattenVarargsArray(arr) {
Expand Down Expand Up @@ -166,7 +162,8 @@ async function base() {
func_name: funcName,
args: args,
caller_address: `${officeApiClient}[${workbookName}]${invocation.address}`, // not available for streaming functions
content_language: contentLanguage,
culture_info_name: await xlwings.getCultureInfoName(),
date_format: await xlwings.getDateFormat(),
version: "placeholder_xlwings_version",
runtime: runtime,
};
Expand Down
42 changes: 39 additions & 3 deletions app/static/js/core/xlwingsjs/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,49 @@ export async function getActiveBookName() {
}
}

// Culture Info Name, e.g., en-DE
let cachedCultureInfoName = null;
export async function getCultureInfoName() {
if (cachedCultureInfoName) {
return cachedCultureInfoName;
}
if (!Office.context.requirements.isSetSupported("ExcelApi", "1.12")) {
return null;
}
const context = new Excel.RequestContext();
context.application.cultureInfo.load(["name"]);
await context.sync();
cachedCultureInfoName = `${context.application.cultureInfo.name}`;
return cachedCultureInfoName;
}

// Date format
let cachedDateFormat = null;
export async function getDateFormat() {
if (cachedDateFormat) {
return cachedDateFormat;
}
if (!Office.context.requirements.isSetSupported("ExcelApi", "1.12")) {
return null;
}
const context = new Excel.RequestContext();
context.application.cultureInfo.datetimeFormat.load(["shortDatePattern"]);
await context.sync();
cachedDateFormat = `${context.application.cultureInfo.datetimeFormat.shortDatePattern}`;
return cachedDateFormat;
}

export function printSupportedApiVersions() {
const versions = [...Array(30)].map((_, i) => `1.${i}`);

function printBuildInfo() {
async function printBuildInfo() {
if (Office.context.diagnostics) {
console.log(`Office Build: ${Office.context.diagnostics.version}`);
console.log(`Office Platform: ${Office.context.diagnostics.platform}`);
console.log(
`Culture Info Name: ${(await getCultureInfoName()) || "N/A"}`,
);
console.log(`Local Date Format: ${(await getDateFormat()) || "N/A"}`);
}
}

Expand All @@ -40,8 +76,8 @@ export function printSupportedApiVersions() {
}
}

Office.onReady(() => {
printBuildInfo();
Office.onReady(async () => {
await printBuildInfo();
const apiNames = [
"ExcelAPI",
"SharedRuntime",
Expand Down
47 changes: 9 additions & 38 deletions app/static/js/core/xlwingsjs/xlwings.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { xlAlert } from "./alert.js";
import { getAccessToken } from "./auth.js";
export { getAccessToken };
import { getActiveBookName, printSupportedApiVersions } from "./utils.js";
export { getActiveBookName };
import {
getActiveBookName,
printSupportedApiVersions,
getCultureInfoName,
getDateFormat,
} from "./utils.js";
export { getActiveBookName, getCultureInfoName, getDateFormat };

// Prints the supported API versions into the Console
printSupportedApiVersions();
Expand Down Expand Up @@ -37,6 +42,8 @@ const xlwings = {
getBookData,
runActions,
pyscriptAllDone,
getCultureInfoName,
getDateFormat,
};
globalThis.xlwings = xlwings;

Expand Down Expand Up @@ -593,42 +600,6 @@ Object.assign(globalThis.callbacks, funcs);

// Callbacks
async function setValues(context, action) {
// Handle DateTime (TODO: backend should deliver indices with datetime obj)
let dt;
let dtString;
action.values.forEach((valueRow, rowIndex) => {
valueRow.forEach((value, colIndex) => {
if (
typeof value === "string" &&
value.length > 18 &&
value.includes("T")
) {
dt = new Date(Date.parse(value));
// Excel on macOS does use the wrong locale if you set a custom one via
// macOS Settings > Date & Time > Open Language & Region > Apps
// as the date format seems to stick to the Region selected under General
// while toLocaleDateString then respects the specific selected language.
// Providing Office.context.contentLanguage fixes this but isn't available for
// Office Scripts
// https://learn.microsoft.com/en-us/office/dev/add-ins/develop/localization#match-datetime-format-with-client-locale
dtString = dt.toLocaleDateString(Office.context.contentLanguage);
// Note that adding the time will format the cell as Custom instead of Date/Time
// which xlwings currently doesn't translate to datetime when reading
if (dtString !== "Invalid Date") {
if (
dt.getHours() +
dt.getMinutes() +
dt.getSeconds() +
dt.getMilliseconds() !==
0
) {
dtString += " " + dt.toLocaleTimeString();
}
action.values[rowIndex][colIndex] = dtString;
}
}
});
});
let range = await getRange(context, action);
range.values = action.values;
await context.sync();
Expand Down
2 changes: 1 addition & 1 deletion docs/custom_functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ def pytoday():
return dt.date.today()
```

By default, it will format the date according to the content language of your Excel instance, but you can also override this by explicitly providing the `date_format` option:
By default, it will format the date according to the cultural info of your Excel instance, but you can also override this by explicitly providing the `date_format` option or the `XLWINGS_DATE_FORMAT` environment variable:

```python
import datetime as dt
Expand Down
6 changes: 3 additions & 3 deletions requirements-core.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ aiocache==0.12.3
# via -r requirements-core.in
annotated-types==0.7.0
# via pydantic
anyio==4.7.0
anyio==4.8.0
# via
# httpx
# starlette
Expand Down Expand Up @@ -61,7 +61,7 @@ packaging==24.2 ; sys_platform != 'win32'
# via gunicorn
pycparser==2.22 ; platform_python_implementation != 'PyPy'
# via cffi
pydantic==2.10.4
pydantic==2.10.5
# via
# fastapi-slim
# pydantic-settings
Expand Down Expand Up @@ -103,7 +103,7 @@ uvloop==0.21.0 ; sys_platform != 'win32'
# via -r requirements-core.in
wsproto==1.2.0
# via simple-websocket
xlwings==0.33.5
xlwings==0.33.6
# via -r requirements-core.in

# The following packages were excluded from the output:
Expand Down
10 changes: 5 additions & 5 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ annotated-types==0.7.0
# via
# -r requirements.txt
# pydantic
anyio==4.7.0
anyio==4.8.0
# via
# -r requirements.txt
# httpx
Expand Down Expand Up @@ -70,7 +70,7 @@ httptools==0.6.4
# via -r requirements.txt
httpx==0.28.1
# via -r requirements.txt
identify==2.6.4
identify==2.6.5
# via pre-commit
idna==3.10
# via
Expand Down Expand Up @@ -118,7 +118,7 @@ pycparser==2.22 ; platform_python_implementation != 'PyPy'
# via
# -r requirements.txt
# cffi
pydantic==2.10.4
pydantic==2.10.5
# via
# -r requirements.txt
# fastapi-slim
Expand Down Expand Up @@ -194,15 +194,15 @@ uvicorn==0.34.0
# via -r requirements.txt
uvloop==0.21.0 ; sys_platform != 'win32'
# via -r requirements.txt
virtualenv==20.28.0
virtualenv==20.28.1
# via pre-commit
watchfiles==1.0.3
# via -r requirements-dev.in
wsproto==1.2.0
# via
# -r requirements.txt
# simple-websocket
xlwings==0.33.5
xlwings==0.33.6
# via -r requirements.txt

# The following packages were excluded from the output:
Expand Down
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ annotated-types==0.7.0
# via
# -r requirements-core.txt
# pydantic
anyio==4.7.0
anyio==4.8.0
# via
# -r requirements-core.txt
# httpx
Expand Down Expand Up @@ -95,7 +95,7 @@ pycparser==2.22 ; platform_python_implementation != 'PyPy'
# via
# -r requirements-core.txt
# cffi
pydantic==2.10.4
pydantic==2.10.5
# via
# -r requirements-core.txt
# fastapi-slim
Expand Down Expand Up @@ -163,7 +163,7 @@ wsproto==1.2.0
# via
# -r requirements-core.txt
# simple-websocket
xlwings==0.33.5
xlwings==0.33.6
# via -r requirements-core.txt

# The following packages were excluded from the output:
Expand Down

0 comments on commit beb146a

Please sign in to comment.